geometry_polygon_object

Explains the user-editable polygon object model of the Cinema API.

geometry_polygon_object.py

#coding: utf-8
"""Explains the user-editable polygon object model of the Cinema API.

The example constructs a simple polygonal cube object.

Topics:
    * Constructing polygon objects
    * c4d.PointObject
    * c4d.PolygonObject
    * c4d.CPolygon

Examples:
    * ConstructPolygonObject(): Demonstrates constructing polygon objects at the example of a cube.

Overview:
    User-editable polygon objects are represented by the type `PolygonObject` which is derived from 
    `PointObject`. `PolygonObject` instances carry points and polygons expressed as lists of 
    `Vector` and `CPolygon` instances. But there is no type which represent edges, as edges are only
    a virtual concept that is only indirectly accessible through selections.

    The vertex and polygon normals which are formed by a PolygonObject are not exposed directly. 
    Instead, only sets of (possibly) interpolated vertex normals are accessible via either a 
    `PhongTag` or `NormalTag` attached to the polygon object; but there is no guarantee that these
    tags are present. Which in turn means that the non-interpolated vertex or polygon normals must
    be computed manually.
"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright (C) 2026 MAXON Computer GmbH"
__date__ = "08/04/2022"
__license__ = "Apache-2.0 License"
__version__ = "S26"

import c4d
import typing

doc: c4d.documents.BaseDocument  # The currently active document.
op: typing.Optional[c4d.BaseObject]  # The selected object within that active document. Can be None.


def ConstructPolygonObject() -> None:
    """Demonstrates constructing polygon objects at the example of a cube.

    Args:
        doc: The active document.
    """
    print ("\n\n--- ConstructPolygonObject Example -----------------------------------------------")

    # Polygon objects are represented by the type PolygonObject. The two arguments to the 
    # initializer of the type describe the point (first) and polygon (second) count the object will
    # be able to hold.
    cube = c4d.PolygonObject(pcnt=8, vcnt=6)
    if not isinstance(cube, c4d.PolygonObject):
        raise MemoryError("Could not allocate polygon object.")

    # PolygonObject is derived from PointObject and provides therefore access to a set of points for
    # the vertices of the object. The points are expressed as c4d.Vector instances in the local 
    # space of the object.
    points = cube.GetAllPoints()

    # The transform of the object, i.e., what is shown as the axis of the object in the viewport.
    # All vectors in #points are expressed in relation to this coordinate system.
    mg = cube.GetMg()

    # A polygon object also provides access to a set of polygons.
    polygons = cube.GetAllPolygons()

    # But there is no such thing as GetAllEdges(), as edges are only implicitly defined by 
    # neighbouring indices in `CPolygon` instances.
    print (f"\nPoints and polygons of '{cube.GetName()}' after allocation:")
    print(f"{cube.GetAllPoints() = }")
    print(f"{cube.GetAllPolygons() = }")

    # One method to generate new geometry is to manually construct it. It is not necessary to 
    # retrieve the existing point and polygon lists of a polygon object for that, unless it is the 
    # explicit goal to extend existing polygonal geometry.

    # The following code will construct a simple cube geometry with this layout:
    #
    #       3 ------- 2
    #      /|        /|
    #     / |       / |
    #    0 ------- 1  |
    #    |  7 -----|- 6
    #    | /       | /
    #    |/        |/
    #    4 ------- 5
    #
    # Where the point #4 is also the origin of the object.

    # The side length of the cube.
    size = 100.0

    # The points of the cube.
    points = [
        c4d.Vector(0, size, 0),            # Point 0
        c4d.Vector(size, size, 0),         # Point 1
        c4d.Vector(size, size, size),      # Point 2
        c4d.Vector(0, size, size),         # Point 3
        c4d.Vector(0, 0, 0),               # Point 4
        c4d.Vector(size, 0, 0),            # Point 5
        c4d.Vector(size, 0, size),         # Point 6
        c4d.Vector(0, 0, size),            # Point 7
    ]

    # Polygons are represented by the type CPolygon. A CPolygon instance does index four points in
    # the PolygonObject (which is also a PointObject) it is attached to.

    # A CPolygon instance, its index values become only meaningful in conjunction with a set of
    # points provided by a PolygonObject.
    quad = c4d.CPolygon(0, 1, 2, 3)

    # The type provides access to the four fields #a, #b, #c, and #d. The fields store the indices
    # of the points the polygon is referencing, not the actual points.
    print (f"\n{quad.a = }, {quad.b = }, {quad.c = }, {quad.d = }")

    # The order of point indices matters, inverting the point order will also invert the normal of 
    # the polygon.
    inverted = c4d.CPolygon(3, 2, 1, 0)

    # A `CPolygon` storing a triangle will repeat its third point index in its field #d.
    tri = c4d.CPolygon(0, 1, 2, 2)

    # This can also be tested with the method IsTriangle() instead of comparing #c and #d.
    print (f"\n{quad.IsTriangle() = }")
    print (f"{tri.IsTriangle() = }")

    # Constructing the polygons for the cube:
    #
    #
    #       3 ------- 2
    #      /|        /|
    #     / |       / |
    #    0 ------- 1  |
    #    |  7 -----|- 6
    #    | /       | /
    #    |/        |/
    #    4 ------- 5
    #

    polygons = [
        # Constructing the first polygon which makes up the top of the cube. 
        #
        #       3 ------- 2
        #      /|        /|
        #     / |       / |
        #    0 ------- 1  |
        #    |         |
        #
        # The order (3, 2, 1, 0) in which the points are being placed might seem counter-intuitive. 
        # This order is caused by the fact that the point order determines the normal of the 
        # polygon. The tuples (2, 1, 0, 3), (1, 0, 3, 2), and (0, 3, 2, 1) would all be valid 
        # alternatives to represent this polygon, but the tuple (0, 1, 2, 3) would for example be 
        # not. 
        # 
        # The normal of a polygon is determined by the cross product of the edges of its vertices.
        # For the edges #a = (0, 1) and #b = (2, 1), the normal of the polygon would face downwards.
        # However, when switching the order of the two edges, and #a is (2, 1) and #b is (0, 1), 
        # the normal will face upwards, the direction the normal of that polygon should point to. 
        # The vertices must therefore be placed in a clockwise order (in relation to the diagram) 
        # to have the polygon normal face into the desired direction. 
        c4d.CPolygon(3, 2, 1, 0),

        # The bottom side. There is no need for putting the polygons in any 'logical' order, a 
        # polygon array can jump all over the place in its geometry, but the order in which the 
        # polygons are placed in the list will determine the polygon index of each polygon, so it 
        # might be desirable to place them in a logical order for the convenience of the user.
        #
        #       |         |
        #    |  7 -----|- 6
        #    | /       | /
        #    |/        |/
        #    4 ------- 5
        c4d.CPolygon(4, 5, 6, 7),

        # The left side.
        #
        #       3 ---
        #      /|
        #     / |
        #    0 --- 
        #    |  7 ---
        #    | /     
        #    |/ 
        #    4 ---
        c4d.CPolygon(0, 4, 7, 3),

        # The right side.
        #
        #             --- 2
        #                /|
        #               / |
        #          --- 1  |
        #             -|- 6
        #              | /
        #              |/
        #          --- 5
        c4d.CPolygon(1, 2, 6, 5),

        # The front side.
        #
        #     /         / 
        #    0 ------- 1  
        #    |         |
        #    |         | 
        #    |         |/
        #    4 ------- 5
        c4d.CPolygon(0, 1, 5, 4),

        # The back side.
        #
        #       3 ------- 2
        #      /|        /|
        #       |         |
        #       |         |
        #       7 ------- 6
        #      /         /
        c4d.CPolygon(3, 7, 6, 2),
    ]

    # The points of a point object can be set at once with the method PointObject.SetAllPoints().
    cube.SetAllPoints(points)

    # But polygons must be set one by one with PolygonObject.SetPolygon() in the Python API.
    for i, cpoly in enumerate(polygons):
        cube.SetPolygon(i, cpoly)

    # After updating the vertices of a PointObject, the message MSG_UPDATE should be sent to it.
    cube.Message(c4d.MSG_UPDATE)

    print (f"\nPoints and polygons of '{cube.GetName()}' after modification:")
    print(f"{cube.GetAllPoints() = }")
    print(f"{cube.GetAllPolygons() = }")

    return cube

def main(doc: c4d.documents.BaseDocument) -> None:
    """Runs the example.Fi

    Args:
        doc: The active document.
    """
    # Run the example constructing a polygon object.
    cube = ConstructPolygonObject()

    # Insert the polygon object into the document, wrapping the operation into an undo.
    if not doc.StartUndo():
        raise RuntimeError("Could not open undo stack.")

    doc.InsertObject(cube)

    if not doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, cube):
        raise RuntimeError("Could not add undo item.")

    if not doc.EndUndo():
        raise RuntimeError("Could not close undo stack.")

    # Set the document to polygon mode and select the new object.
    doc.SetMode(c4d.Mpolygons)
    doc.SetActiveObject(cube, c4d.SELECTION_NEW)

    # Inform Cinema 4D that the document has been modified.
    c4d.EventAdd()


if __name__ == '__main__':
    c4d.CallCommand(13957) # Clear the console.
    # doc is a predefined module attribute as defined at the top of the file.
    main(doc)