Solved Generator sticks in a dirty loop

Dear Python experts,

I am working on a Python Generator Plugin which creates Splines from Polygon Edges including some cleanup procedures. The problem I am facing is that the Generator works in a simple setup (e.g. it has a Voronoi Fracture or Clone under it) but when it comes in complex setups with more children it spins in an endless dirty loop. It really gets evil when Fields are involved in that setup. I tried everything including SetOptimizeCache(True) or checking dirty states of the children with a loop but nothing really helped here. Maybe it is because of all these modelling commands which probably make a scene dirty.

Could you provide me with some help? I know it is a generic question, I would like to be more specific but I don't know how. I would really appreciate your help here.

Here is the code of the entire plugin:

import os
import c4d
from c4d import plugins, utils, bitmaps
from c4d.utils import SplineHelp

class SplFromEdge(plugins.ObjectData):

    def Init(self, node):
        # Retrieves the BaseContainer Instance to set the default values
        #self.SetOptimizeCache(True)
        data = node.GetDataInstance()
        if data is None:
            return False
        
        data.SetBool(c4d.WELDSIMILAR, False)
        data.SetFloat(c4d.THRESHOLD, 0.01)
        data.SetInt32(c4d.O_SPLINEFROMEDGEMODE, c4d.O_SPLINEFROMEDGEMODE_TAG)
        data.SetBool(c4d.O_SPLINEFROMEDGEMODE_AUTOUPDATE , True)
        data.SetBool(c4d.CLOSE,True)
        data.SetBool(c4d.O_SPLINEFROMEDGEMODE_OPTIMIZE, True)
        return True

    #grey out the threshold in the UI
    def GetDEnabling(self, node, id, t_data, flags, itemdesc):
        data = node.GetDataInstance()
        weld = data.GetBool(c4d.WELDSIMILAR)
        if id[0].id == c4d.THRESHOLD:
            return weld

        # Retrieves the current interpolation
        inter = node[c4d.O_SPLINEFROMEDGEMODE]

        # Defines enable state for the selection
        if id[0].id == c4d.EDGESELECTION:
            return inter == c4d.O_SPLINEFROMEDGEMODE_TAG
        return True


    #iterator to go through the whole hierarchy
    def walk(self,op) :  
        if not op: return  
        elif op.GetDown() :  
            return op.GetDown()  
        while op.GetUp() and not op.GetNext():  
            op = op.GetUp()  
        return op.GetNext()  
    
    #has all modeling operators
    def ExecModelingCommand(self, doc,opCtrl, op, parent):
        if op is None:
            return

        data = opCtrl.GetDataInstance()
        threshold = data.GetReal(c4d.THRESHOLD)
        weld = data.GetBool(c4d.WELDSIMILAR)
        mode = data.GetInt32(c4d.O_SPLINEFROMEDGEMODE)
        selectionSet = data.GetString(c4d.EDGESELECTION)
        optimize = data.GetBool(c4d.O_SPLINEFROMEDGEMODE_OPTIMIZE)
        close = data.GetBool(c4d.CLOSE)
        
        splineHelper = SplineHelp()
        container = c4d.BaseContainer()
        
        childObject = op.GetClone(c4d.COPYFLAGS_NONE)
        # Creates a temporary document.
        tempDoc = c4d.documents.BaseDocument()
        # Insert the cloned object to the temporary document.
        tempDoc.InsertObject(childObject)
        
        tempParent = c4d.BaseObject(c4d.Onull)

        if childObject is None or (mode==c4d.O_SPLINEFROMEDGEMODE_TAG and selectionSet==""):
            return
 
        parent.SetMg(childObject.GetMg())

        c4d.StatusSetText("Creating Splines from Edges")
        c4d.StatusSetSpin()

        res = utils.SendModelingCommand(command=c4d.MCOMMAND_CURRENTSTATETOOBJECT,
                                    list=[childObject],
                                    mode=c4d.MODELINGCOMMANDMODE_ALL,
                                    bc=container,
                                    doc=tempDoc)

        if res is None:
            return
        
        editableObject = res[0]
        #select edges based on a selection tag
        def selectEdges(obj,tag):
            selection = tag.GetBaseSelect()
            polyselection = obj.GetEdgeS()
            selection.CopyTo(polyselection)

        #iterate through the new created polygon objects and look for the ones with an edge selection tag
        objectList = []   

        while editableObject:
            if editableObject.CheckType(c4d.Opolygon):
                if mode==c4d.O_SPLINEFROMEDGEMODE_TAG:
                    for i in editableObject.GetTags():
                        if i.GetName()==selectionSet and i.CheckType(c4d.Tedgeselection):
                            #select
                            selectEdges(editableObject,i)
                            #add to list                        
                            objectList.append(editableObject)
                elif mode==c4d.O_SPLINEFROMEDGEMODE_ALL:
                    utils.SendModelingCommand(c4d.MCOMMAND_SELECTALL, 
                                            list = [editableObject], 
                                            mode = c4d.MODELINGCOMMANDMODE_EDGESELECTION, 
                                            bc = c4d.BaseContainer())
                    objectList.append(editableObject)
            editableObject = self.walk(editableObject)
       
        if not objectList:
            c4d.StatusClear()
            return

        for i in objectList:
            i.InsertUnderLast(tempParent)

        spl = utils.SendModelingCommand(command=c4d.MCOMMAND_EDGE_TO_SPLINE,
                                            list=objectList,
                                            mode=c4d.MODELINGCOMMANDMODE_ALL,
                                            doc=objectList[0].GetDocument()) 

        c4d.StatusSetText("Optimizing Spline Objects")
        
        #Exploding Splines with multiple Segments which came from Edge to Spline
        splitList = []
        for i in objectList:
            child = i.GetDown()
            if child is not None:
                if child.CheckType(c4d.Ospline):

                    splineHelper.InitSpline(child)
                    if splineHelper.GetSegmentCount() !=1:
                        splitList.append(child)
 
        #Explode Splines using the Explode Command
        if splitList:
            expSplines = utils.SendModelingCommand(command=c4d.MCOMMAND_EXPLODESEGMENTS,
                                                                    list=splitList,
                                                                    mode=c4d.MODELINGCOMMANDMODE_ALL,
                                                                    flags=c4d.MODELINGCOMMANDFLAGS_NONE,
                                                                    doc=splitList[0].GetDocument()) 
        
        #Creating a Spline List which will be later added under a parent object
        splineList = []
        obj=objectList[0]
        while obj:
            if obj.CheckType(c4d.Ospline):
                splineHelper.InitSpline(obj)
                if splineHelper.GetSegmentCount() !=0:
                    #Add all spline objects to a dedicated spline list. Every object which is not in that list will be deleted
                    splineList.append(obj)
            obj = self.walk(obj)

        #optimize the spline for duplicate points
        optimizeBC = c4d.BaseContainer()
        optimizeBC[c4d.MDATA_OPTIMIZE_POINTS]= optimize
        optimizeBC[c4d.MDATA_OPTIMIZE_UNUSEDPOINTS]= True
        optimizeBC[c4d.MDATA_OPTIMIZE_TOLERANCE]= 0.01
        optimizedSplines = utils.SendModelingCommand(command=c4d.MCOMMAND_OPTIMIZE,
                                                                    list=splineList,
                                                                    bc= optimizeBC,
                                                                    mode=c4d.MODELINGCOMMANDMODE_ALL,
                                                                    flags=c4d.MODELINGCOMMANDFLAGS_NONE)
        
        #filter overlapping objects
        c4d.StatusSetText("Cleaning Up and Welding")

        positionList = []
        keyList = []
        noDuplicateSplineList = []
        for i in splineList:
            points = i.GetAllPoints()
            bary = c4d.Vector(0)
            #find the center of all points            
            for j in points:
                bary+=j
            bary = bary / len(points)
            # move the points by the amount represented by the new axis center              
            for index, point in enumerate(points) :  
                point = point - bary 
                i.SetPoint(index, point)  
            # notify about the points repositioning  
            i.Message(c4d.MSG_UPDATE)  
            # adjust the matrix offset component accordingly 
            currentMg = i.GetMg() 
            currentMg.off += bary
            # reposition the object in the space 
            i.SetMg(currentMg)
            #adjust object position
            polyParent = i.GetUp()
            offset = polyParent.GetMg().off
            #insert and adjust object position
            i.InsertUnderLast(tempParent)
            i[c4d.SPLINEOBJECT_CLOSED]=close
            currentMg = i.GetMg()
            currentMg.off += offset
            i.SetMg(currentMg)
            #remove overlapping splines if welding is active
            if weld:
                #check spline object position if it mateches a certain threshold
                overlapping = False
                if positionList:
                    for j in positionList:
                        p1 = i.GetMg().off
                        p2 = j.GetMg().off
                        distance = (p2-p1).GetLength()
                        
                        if distance <= threshold: 
                            overlapping = True  
                #check if it's overlapping and clear it
                key = int((i.GetRad().x+i.GetRad().y+i.GetRad().z)/threshold)
                if key in keyList and overlapping:
                    i.Remove()                    
                else:
                    keyList.append(key)
                    positionList.append(i)
                    noDuplicateSplineList.append(i)
            else:
                noDuplicateSplineList.append(i)
        c4d.StatusClear()
        
        #put everything under the parent null and return it
        for i in noDuplicateSplineList:
            i.InsertUnderLast(parent)

        return parent
    
    def GetVirtualObjects(self, op, hh):

        doc = op.GetDocument()
        data = op.GetDataInstance()       

        objInput = op.GetDown()
        
        if objInput is None:
            return None
        
        objRet = c4d.BaseObject(c4d.Onull)
        if doc is None or objRet is None:
            return None

        
        hierarchyClone = op.GetAndCheckHierarchyClone(hh, objInput, c4d.HIERARCHYCLONEFLAGS_ASPOLY, True)
        
        if hierarchyClone["dirty"] is False:
            return hierarchyClone["clone"]
            
        clone = hierarchyClone["clone"]
                
        if clone is None:
            return op.GetCache()
    
        self.ExecModelingCommand(doc, op, clone, objRet)
        return objRet

###############################################################################################################
# Plugin Registration
###############################################################################################################

PLUGIN_ID_GENERATOR = 954679

def RegisterObjectData(id, name, bmpPath, objData, desc, flags):
    bmp = bitmaps.BaseBitmap()
    bmp.InitWith(os.path.join(bmpPath, "res", "icon.tif"))
    plugins.RegisterObjectPlugin(id=id, str=name,
                                g=objData,
                                description=desc, icon=bmp,
                                info=flags)

if __name__ == "__main__":
    path, fn = os.path.split(__file__)

    RegisterObjectData(PLUGIN_ID_GENERATOR, "Spline from Edge", path, SplFromEdge, "osplinefromedge", c4d.OBJECT_GENERATOR | c4d.OBJECT_INPUT)

Hi @matniedoba, thanks for reaching out us.

With regard to your issue, given that it's not our primary task to debug plugins, to understand what's getting wrong I need you to provide:

  • test scenes of different complexity;
  • the smallest code able to reproduce the issue - rather than the whole plugin - considering that it's a matter of object dirty check.

I also second @zipit recommendations because improving code design via better function abstraction and documentation will lead to an increased clarity, reliability and, in the end, comprehension.
I'll try to find time in the course of the week to have a look at your issue given the items requested above.

Best, R

Hi,

I think your biggest problem is your coding style. And I do not mean that in an offensive way, but simply am pointing out that you seem to have cornered yourself into a state of code that contains bugs where you (and also I) have no clue where they exactly stem from. You might want to read PEP 8 and the excellent Google Python Style Guide.

With that out of the way, the obvious thing first: When you use the dirty state of the children of your generator to determine if it has to be rebuild or not, either touching one of the children or having a child which determines its dirty state by its parent state (i.e. the generator), will result in a loop.

Some points:

  1. Returning None in GetVirtualObjects indicates a memory error to Cinema 4D. You rarely want to do this. Return a c4d.Onull-object instead. Also neither doc nor objRet can be None in your GVO, so checking for that is somewhat moot.
  2. You clone the children of your generator with GetAndCheckHierarchyClone and one of the first things you do in ExecModelingCommand is to clone that clone again without ever using or comparing against that first clone again. This obviously won't break anything, but it is a good indicator for being lost in ones own code.

I do not have any more specific advice, since I stopped reading at some point as it got more and more convoluted (sorry, no offense intended). Instead I have some general advice:

  1. You probably need some abstraction or specifically split up ExecModelingCommand into its steps, so that you can describe and test each part.
  2. Follow a style and write some documentation (for yourself as one tends to overlook or forget things when they are not written down).
  3. Since your intention seems to be to return a spline, you should overwrite GetContour and not GVO.
  4. While your approach is valid, the heavy use of SendModellingCommand is probably not the best for the performance of your plugin and also a source of uncertainty for debugging. Creating a consecutive line segment array (i.e a linear spline) from an edge selection is not that hard. By doing so, you would also remove the need of any clean up clutter.

I hope this helps.

Cheers,
zipit

MAXON SDK Specialist
developers.maxon.net

Hi @matniedoba, thanks for reaching out us.

With regard to your issue, given that it's not our primary task to debug plugins, to understand what's getting wrong I need you to provide:

  • test scenes of different complexity;
  • the smallest code able to reproduce the issue - rather than the whole plugin - considering that it's a matter of object dirty check.

I also second @zipit recommendations because improving code design via better function abstraction and documentation will lead to an increased clarity, reliability and, in the end, comprehension.
I'll try to find time in the course of the week to have a look at your issue given the items requested above.

Best, R

Thank you @zipit for your comprehensive response, especially with the link to the coding guide. I guess it might be better to create the spline manually from the points which are connected by the edges without the usage of SendModellingCommand. It was good for prototyping functionality but as you mentioned, it is hard to debug. I appreciate you took the time (and I know it's a lot of convoluted code) to read through it. @r_gigante -> the work is currently on my side so you don't need to bother. I think I have enough information to do a second iteration and rewriting this from scratch.