modeling_command_extrude

Demonstrates how to use the SendModelingCommand (SMC) function in general at the example of the ‘Extrude’ tool.

modeling_command_extrude.py

#coding: utf-8
"""Demonstrates how to use the `SendModelingCommand` (SMC) function in general at the example of the 
'Extrude' tool.

Extrudes the active selection of the currently selected polygon object in the mode the active 
document is currently in (point, edge, or polygon mode). The active document has to be in one of
these three modes for the script to succeed.

Topics:
    * The 'Extrude' tool
    * c4d.utils.SendModelingCommand()

Overview:
    There are in principle two ways to execute modeling commands: In the original document a node
    is contained in or in a dummy document. The latter case is carried out by creating a dummy 
    document just for the command, cloning the relevant node(s) into that document, and then 
    executing the command there. In most cases, executing a modeling command in the original 
    document - which could be a loaded document and therefore subject to threading restrictions - 
    will cause no issues, but there are some notable exceptions:
    
      * SMC calls which are not done from a non-main thread context for a node in a loaded document.
        This could for example be an SMC call in an object plugins GetVirtualObjects method that is
        meant to modify an input object of the plugin. Think of an extrude object plugin that has 
        for example the option to bevel its input spline.
      * Tools or tasks for which the complexity of a document directly impacts runtime of the tool
        but not the result of the tool; imagine a huge amount of irrelevant data the tool might has 
        to traverse.
      * Some tools require setups which make a dummy document desirable, e.g., the 'Join' tool.
    
    But using a dummy document has also drawbacks:
      
       * It is computationally much more demanding since all relevant data has first to be copied 
         over to the dummy document and often then also back again to an 'original' document.
       * Inserting the SMC result back into its original document can be labour-intensive when the
         result is meant to replace the original object and cannot be inserted as a new object. For 
         example all BaseLink parameters in that document which link to that node must then be found
         and updated too.
       * Determining what is relevant for a tool can be hard. In some cases it might not be enough
         to copy over a single object, and instead non-obvious dependencies which influence the 
         outcome of the tool are required too. The 'Current State to Object' tool is such example, 
         as the state of an object can depend on many things as for example fields, effectors, and 
         tags. In these cases it is best to clone the whole document; documents are nodes and can 
         therefore be cloned themselves. But it will further increase the time and memory complexity
         problem of the approach.
    
    Showcased in this example is primarily the more complicated case of executing the command in a 
    temporary document. But in general it is more desirable to execute SMC in the document the node
    is already contained in.
"""
__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 main(doc: c4d.documents.BaseDocument, op: typing.Optional[c4d.BaseObject]) -> None:
    """Runs the example.

    Args:
        doc: The active document.
        op: The selected object in #doc. Can be #None.
    """
    if not isinstance(op, c4d.PolygonObject):
        raise TypeError(f"Please select a {c4d.PolygonObject} instance.")

    # Clone the node which is the target of the operation and insert it into a temporary document.
    clone = op.GetClone(c4d.COPYFLAGS_NONE)
    if not isinstance(clone, c4d.PolygonObject):
        raise RuntimeError(f"Could not clone input object {op}.")

    temp = c4d.documents.BaseDocument()
    if not isinstance(temp, c4d.documents.BaseDocument):
        raise MemoryError("Could not create new document.")
    temp.InsertObject(clone)

    # A modeling command invokes one of the modeling tools of Cinema 4D. The function takes
    # therefore container for the settings of that tool, in this case for the Extrude tool.
    bc = c4d.BaseContainer()
    bc[c4d.MDATA_EXTRUDE_PRESERVEGROUPS] = True # Preserve element groups in extrusions.
    bc[c4d.MDATA_EXTRUDE_OFFSET] = 50.0 # The extrusion depths.
    # There are many more options for that tool as exposed in toolextrude.h

    # A modeling command takes a list of objects as one of its inputs. In this case it is just a 
    # list containing the node #clone.
    objects = [clone]

    # A modeling command has also a mode of operation. The extrude tool for example behaves 
    # differently, depending on in which editor mode (point, edge, polygon, object) the document 
    # is in. E.g., being in edge mode will extrude the active edge selection. The example here ties 
    # the SMC mode to the mode of active document. There are also more SMC modes than shown here.
    docMode = doc.GetMode()
    if docMode == c4d.Mpoints:
        mode = c4d.MODELINGCOMMANDMODE_POINTSELECTION
    elif docMode == c4d.Medges:
        mode = c4d.MODELINGCOMMANDMODE_EDGESELECTION
    elif docMode == c4d.Mpolygons:
        mode = c4d.MODELINGCOMMANDMODE_POLYGONSELECTION
    else:
        raise RuntimeError(f"Document is not in any of the modes supported by the extrude tool.")

    # Finally, a modeling command also takes a set of flags. With them the command can for example
    # be automatically wrapped in an undo. Since this example operates on a temporary 'throw-away'
    # document, the flags are being set here to the none-flag.
    flags = c4d.MODELINGCOMMANDFLAGS_NONE

    # Execute the command. Most commands return a boolean indicating the success of the operation,
    # but some commands also return collections, containing more data. See the #MCOMMAND 
    # documentation for information on a per command basis.
    res = c4d.utils.SendModelingCommand(command=c4d.ID_MODELING_EXTRUDE_TOOL, list=objects, 
                                        mode=mode, bc=bc, doc=temp, flags=flags)
    if not res:
        raise RuntimeError(f"modeling command failed for {op}.")

    # The same call when not going the temporary document route, and instead carrying out the 
    # command directly on the node in the active document. The the file documentation for details.

    # res = c4d.utils.SendModelingCommand(command=c4d.ID_MODELING_EXTRUDE_TOOL, list=[op], 
    #                                     mode=mode, bc=toolSettings, doc=doc, flags=flags)

    # The operation has been successful at this point and #clone has now been extruded. The node can
    # be removed from the temporary document and inserted back into the original document. This
    # example will take the easy route and just insert #clone as a new object alongside the input
    # node #op.
    clone.Remove()

    # Open an undo.
    if not doc.StartUndo():
        raise RuntimeError("Could not open undo stack.")

    # Rename the node and insert it back.
    clone.SetName(f"{clone.GetName()} (SMC)")
    doc.InsertObject(clone)

    # Add an undo and close the stack.
    if not doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, clone):
        raise RuntimeError("Could not add undo item.")

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

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


if __name__ == '__main__':
    # #doc and #op are predefined module attributes as defined at the top of the file.
    main(doc, op)