SOLVED Set c4d.InExcludeData() has something didn't expect.

Hi !

Question :

I try to insert a plain effector into a cloner InExcludeData , It worked , but sometimes i have to click twice but no error ( seems sometimes it clear the InExcludeData ) .

How can I fix it ?

@Windows11 22H1 C4D R2023.0.0

Many Thanks

Code

from typing import Optional
import c4d
from math import radians

doc: c4d.documents.BaseDocument  # The active document
op: Optional[c4d.BaseObject]  # The active object, None if unselected

def InsertNodeIntoInExclude(data : c4d.InExcludeData, node : c4d.BaseObject, index, isActive=True):
    """Inserts a node at a certain index into an InExcludeData."""
    dataCount = data.GetObjectCount()
    doc = node.GetDocument()

    if index > dataCount:
        raise IndexError(f"The index {index} is out of bounds.")    

    result, i = c4d.InExcludeData(), 0
    while data.ObjectFromIndex(doc, i):
        if i == index:
            result.InsertObject(node, int(isActive))
        
        item = data.ObjectFromIndex(doc, i)
        flags = data.GetFlags(i)
        result.InsertObject(item, flags)
        i += 1
    return result

# 量化旋转
def mogragh_quantize_rotation(doc :c4d.documents.BaseDocument, cloner : c4d.BaseObject) -> c4d.BaseObject :
    """Inserts a quantize rotation effector into selected cloner."""
    doc.StartUndo()

    # Pass if selection is not a Cloner
    if cloner is None or cloner.GetType() != 1018544:
        raise RuntimeError("Select a cloner.")
    
    effector = c4d.BaseObject(1021337) # Plain    
    doc.InsertObject(effector,pred=cloner)
    doc.AddUndo(c4d.UNDOTYPE_NEWOBJ,effector)
    

    # Set plain to cloner InExclude
    inExData = cloner[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST]
    if inExData:
        if inExData.GetObjectCount() == 0:
            try:
                emptyData = c4d.InExcludeData()
                emptyData.InsertObject(effector,1)
                cloner[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST] = emptyData
            except:
                raise RuntimeError("Insert failed , try again .")
        else:
            try:                
                modifiedInExData = InsertNodeIntoInExclude(inExData, effector, 0)
                doc.AddUndo(c4d.UNDOTYPE_CHANGE, cloner)
                cloner[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST] = modifiedInExData 
            except:
                raise RuntimeError("Insert failed , try again .")
    else:
        raise RuntimeError("Failed to get Includedata .")
    
    # FieldObject
    field_object = c4d.modules.mograph.FieldObject(440000281) # ramdom field object
    doc.InsertObject(field_object,parent=effector)
    doc.AddUndo(c4d.UNDOTYPE_NEWOBJ,field_object)
    
    # Set Field List
    fieldList = effector[c4d.FIELDS]    
    layer = c4d.modules.mograph.FieldLayer(c4d.FLfield)
    layer.SetLinkedObject(field_object)
    fieldList.InsertLayer(layer)
    
    # Quantize Layer
    quantizeLayer = c4d.modules.mograph.FieldLayer(c4d.FLquantize)
    if quantizeLayer is None:
        raise MemoryError("Failed to create a Field Layer.")

    fieldList.InsertLayer(quantizeLayer)
    doc.AddUndo(c4d.UNDOTYPE_BITS, quantizeLayer)
    quantizeLayer.SetBit(c4d.BIT_ACTIVE)
    
    # Set
    effector.SetName(cloner.GetName() + ' ' + 'Plain')
    effector[c4d.ID_MG_BASEEFFECTOR_POSITION_ACTIVE] = False # Turn off Pos
    effector[c4d.ID_MG_BASEEFFECTOR_SCALE_ACTIVE] = False # Turn off Scale
    effector[c4d.ID_MG_BASEEFFECTOR_ROTATE_ACTIVE] = True # Turn On Pot
    effector[c4d.ID_MG_BASEEFFECTOR_ROTATION,c4d.VECTOR_X] = radians(360) # Set Rot at H(x) to 360 degrees
    
    field_object[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 1 # Set editor unvisible
    field_object.SetName(effector.GetName() + ' : ' + 'Quantize Rot') # Set Field Name to Effetor name + Function
    
    doc.AddUndo(c4d.UNDOTYPE_CHANGE, effector)
    effector[c4d.FIELDS] = fieldList

    doc.EndUndo()
    c4d.EventAdd() # Refresh Viewport

    return effector


if __name__ == '__main__':
    doc = c4d.documents.GetActiveDocument()
    cloner = doc.GetActiveObject()
    mogragh_quantize_rotation(doc,cloner)

Hey @dunhou,

Thank you for reaching out to us and a special thanks for your postings which are always super clear. I think I already said it before, but it makes my life much easier.

There are sort of two problems in your code:

  1. You set your inExData in a bit odd place, which can lead to update problems in some special cases, when you then after that start constructing your field list setup related to the effector referenced by the InExcludeData inExData. This should not cause problems in most cases, but for safety I would first construct my little effector/field rig in its entirety, and then reference it in the InExcludeData.
  2. Your InsertNodeIntoInExclude produced a few problems for me when I tried your script, I did not try to debug it, and instead just rewrote the function how I would write it.

I have provided below my solution and the script does what I would expect it to do. The "click twice" thing you encountered was likely not an update error, in the sense of Cinema 4D not updating, but an index error due to how you fashioned your InsertNodeIntoInExclude function and then effectively breaking early in it on the InExcludeData iteration when there were some dangling links in it, in short, you made never sure that i reached dataCount - 1 and instead stopped when one of the links in the list returned None.

If this does not solve your problem, I will have to ask for a more precise explanation of what constitutes "sometimes", as for me it does now what I would expect it to do.

Cheers,
Ferdinand

The code:

import c4d
import typing

doc: c4d.documents.BaseDocument  # The active document
op: typing.Optional[c4d.BaseObject]  # The active object, None if unselected

def GetUpdatedInexcludeData(data:c4d.InExcludeData, node:c4d.BaseObject, index:int, 
                            isActive:bool=True) -> c4d.InExcludeData:
    """Inserts a node at a certain index into an InExcludeData.
    
    Note: I had to rewrite this because there were some problems with how the function
    behaved in the case of no or a singular element in the list.
    """
    # Bail when the user tries to add a node which is not part of a document and get the number
    # of elements in #data.
    doc: c4d.documents.BaseDocument = node.GetDocument()
    if not isinstance(doc, c4d.documents.BaseDocument):
        raise RuntimeError("Cannot add dangling node to InExcludeData")

    count: int = data.GetObjectCount()

    # Retrieve all objects in it and their flags as a list of (object, flags) pairs. Object access
    # can fail with ObjectFromIndex(), which is why we have to check if it returns not None, to
    # step over these "dangling" slots. This also implicitly assumes that all nodes in #data are
    # part of the same document #node is part of. To be more robust, you should pass in the document
    # of the node the InExcludeData #data was taken from, and then also test if #node is part of the
    # same document.
    rawData: list[tuple[c4d.BaseList2D, int]] = [
        (data.ObjectFromIndex(doc, i), data.GetFlags(i)) for i in range(count)
        if data.ObjectFromIndex(doc, i) != None
    ]
    # Insert the new node at the desired location in the list. Since Booleans are just integers in
    # disguise, int(#isActive) is equal to #isActive alone, but one can add it for verbosity.
    newIndex: int = index if index < (len(rawData) - 1 ) else 0
    rawData.insert(newIndex, (node, isActive))

    # Construct the new InExcludeData where #node is at #index with the flag #isActive.
    result: c4d.InExcludeData = c4d.InExcludeData()
    for node, flags in rawData:
        result.InsertObject(node, flags)

    return result

# 量化旋转
def mogragh_quantize_rotation(doc :c4d.documents.BaseDocument, cloner : c4d.BaseObject) -> c4d.BaseObject :
    """Inserts a quantize rotation effector into selected cloner."""
    doc.StartUndo()

    # Pass if selection is not a Cloner
    if cloner is None or cloner.GetType() != 1018544:
        raise RuntimeError("Select a cloner.")
    
    effector = c4d.BaseObject(1021337) # Plain    
    doc.InsertObject(effector,pred=cloner)
    doc.AddUndo(c4d.UNDOTYPE_NEWOBJ,effector)

    # --- Change: I removed here code and moved it towards the end of your script -------------------
    # --- End of changes ---------------------------------------------------------------------------
    
    # FieldObject
    field_object = c4d.modules.mograph.FieldObject(440000281) # random field object
    doc.InsertObject(field_object,parent=effector)
    doc.AddUndo(c4d.UNDOTYPE_NEWOBJ,field_object)
    
    # Set Field List
    fieldList = effector[c4d.FIELDS]    
    layer = c4d.modules.mograph.FieldLayer(c4d.FLfield)
    layer.SetLinkedObject(field_object)
    fieldList.InsertLayer(layer)
    
    # Quantize Layer
    quantizeLayer = c4d.modules.mograph.FieldLayer(c4d.FLquantize)
    if quantizeLayer is None:
        raise MemoryError("Failed to create a Field Layer.")

    fieldList.InsertLayer(quantizeLayer)
    doc.AddUndo(c4d.UNDOTYPE_BITS, quantizeLayer)
    quantizeLayer.SetBit(c4d.BIT_ACTIVE)
    
    # Set
    effector.SetName(cloner.GetName() + ' ' + 'Plain')
    effector[c4d.ID_MG_BASEEFFECTOR_POSITION_ACTIVE] = False # Turn off Pos
    effector[c4d.ID_MG_BASEEFFECTOR_SCALE_ACTIVE] = False # Turn off Scale
    effector[c4d.ID_MG_BASEEFFECTOR_ROTATE_ACTIVE] = True # Turn On Pot
    effector[c4d.ID_MG_BASEEFFECTOR_ROTATION,c4d.VECTOR_X] = c4d.utils.DegToRad(360) # Set Rot at H(x) to 360 degrees
    
    field_object[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR] = 1 # Set editor unvisible
    field_object.SetName(effector.GetName() + ' : ' + 'Quantize Rot') # Set Field Name to Effetor name + Function
    
    doc.AddUndo(c4d.UNDOTYPE_CHANGE, effector)
    effector[c4d.FIELDS] = fieldList

    # Set plain to cloner InExclude
    inExData = cloner[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST]
    
    # --- Changes: Your modified and moved code ----------------------------------------------------
    
    # I would add the effector here after you have created all the field object stuff, because it
    # makes a bit more sense to do it once you are done creating the little effector rig, but it 
    # is not technically required in most cases.

    # I am not quite sure why you did all the exception handling stuff in your code here, I assume
    # because you wanted to to capture the case that inExData is not what you expect it to be. I
    # would simply type check here if you want to be sure.
    if not isinstance(inExData, c4d.InExcludeData):
        raise RuntimeError("Whoopsie: ID_MG_MOTIONGENERATOR_EFFECTORLIST does not hold InExcludeData.")
    
    # Use your function to construct an InExcludeData where #effector sits at index 0 while copying
    # over the old data.
    cloner[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST] = GetUpdatedInexcludeData(inExData, effector, 0)

    doc.AddUndo(c4d.UNDOTYPE_CHANGE, cloner)
    # --- End of changes ---------------------------------------------------------------------------

    doc.EndUndo()
    c4d.EventAdd() # Refresh Viewport

    return effector


if __name__ == '__main__':
    doc = c4d.documents.GetActiveDocument()
    cloner = doc.GetActiveObject()
    mogragh_quantize_rotation(doc,cloner)

@ferdinand Thanks for that solution.

It's happy to hear that , I do try to make win-win clear post as I can to don't waste our time , hope it's a worthy work 😊

I tested the new GetUpdatedInexcludeData function , it worked well , The "click twice" thing never happen again . It's the None condition I didn't realize before . It does happend when no or single object in the list .

And for the "odd place" and the "exception ", it is a test to make sure all the things above work well( after that are all the settings ) . I forgot move to the right place . sorry to that

Thanks for the caring code for c4d.utils.DegToRad(360) , I haven't notice this before and just stupid import radians😢