Field Refusing Object
-
Hello @d_schmidt,
this thread has not been overlooked by us; I apologize but we were busy with other stuff. Your question will be the next thing on my desk tomorrow.
Cheers,
Ferdinand -
Hello @d_schmidt,
thank you for reaching out to us and please excuse the delay, but there were some hoops to jump through regarding your question.
First of all,
MSG_DESCRIPTION_CHECKDRAGANDDROP
is in principle the correct approach, but it is not being sent for all description elements which have drag and drop events. This does happen mostly for R13+ elements, i.e., some of the "newer" elements. Currently this is not very well documented, and we will update the documentation forMSG_DESCRIPTION_CHECKDRAGANDDROP
and elements it is not being sent for.There is unfortunately not an equally convenient replacement message for these elements, and one must implement a rejection logic manually. The idea is pretty straight forward. Implement
NodeData::SetParameter
and reject writing a parameter value which does not conform to what one wants to reject. In the case ofFieldList
this can get a bit hairy because iterating over aFieldList
can be non-trivial due to the complexity field lists can have. I cannot provide here a full solution for you, but I did provide an example in Python which also deals with one of the corner cases of aFieledList
- group fields.If you need further help or have trouble with translating the Python example to C++, feel free to ask additional questions.
Cheers,
FerdinandThe behavior of the work-around:
The code:
"""Example for emulating refusing a field layer in field list with NodeData::SetParameter. As discussed in: https://developers.maxon.net/forum/topic/13387/ """ import c4d class PC13387(c4d.plugins.ObjectData): """Example ObjectData plugin that has two parameters. * ID_REJECTION - A base-link for an object to reject. * ID_FIELDLIST - A field list which could contain a a field layer for that object to reject and which we want to prevent. """ ID_PLUGIN = 1057375 @staticmethod def FieldListContains(doc, testNode, fieldList): """Determines if a node is contained in a field list or not. Iterating over field lists is not trivial, and I do not have the time to cover here any corner case, feel free to ask questions once you get stuck. Important to understand is also that a field list does contain field layers which are not equal to their object manager representation. The object manager repr. of a field layer has to be retrieved with GetLinkedObject() if there is one (which is not a given). Args: doc (c4d.documents.BaseDocument): The document the field list is contained in. testNode (c4d.GeListNode): The node to test for. fieldList (c4d.FieldList): The field list to test. Returns: bool: If testNode is contained or not. """ def getFieldLayers(node): """ """ visited = [] end = node.GetUp() if isinstance(node, c4d.GeListNode) else None while node: # If the current node is a newly encountered one. if node not in visited: visited.append(node) # When it is a field layer, yield it and deal with group # fields ... if isinstance(node, c4d.modules.mograph.FieldLayer): yield node # For the special case of a group field, we have to unpack # the data of the field by accessing its content via its # object manager representation. Since what one does see # in the field list, i.e., fields below an group field, is # actually not there, but only a view. The data is stored # with the field list of the group field. So we cannot # access this data by iterating over the field tree we # are currently iterating over, i.e., # myGroupFieldLayer.GetDown() will yield None. # Get the object manger representation of the current # field layer. If there is one and its type is that of # a field group: linkedNode = node.GetLinkedObject(doc) if linkedNode and linkedNode.CheckType(c4d.Fgroup): # Get the field list and iterate over it. fieldList = linkedNode[c4d.FIELDGROUP_FIELDS] root = fieldList.GetLayersRoot() for nestedNode in getFieldLayers(root): yield nestedNode # There are more special cases in the field list hierarchy, # e.g., folders, which also have to be treated specifically. # I did not do that here. # Normal depth-first traversal of a node tree if node.GetDown() and node.GetDown() not in visited: node = node.GetDown() elif node.GetNext(): node = node.GetNext() else: node = node.GetUp() if node == end: return # Get the root layer of the field list and traverse it. root = fieldList.GetLayersRoot() for fieldLayer in getFieldLayers(root): # When the object linked to the field layer is equal to the one # we want to reject, then we return True, as we found it. linkedNode = fieldLayer.GetLinkedObject(doc) if linkedNode == testNode: return True return False def Init(self, node): """Not documented. """ self.InitAttr(node, c4d.BaseList2D, c4d.ID_REJECTION) self.InitAttr(node, c4d.FieldList, c4d.ID_FIELDLIST) return True def SetDParameter(self, node, id, t_data, flags): """Overwrite the process of writing a parameter. When a field list is being modified, e.g., by a drag and drop event, we validate that to be written field list by iterating over it. When we encounter the object we want to reject, we signal that we did carry out the writing of the parameter without doing it, causing the old state of the field list to remain, i.e., the drag and dropped object has been rejected. Args: node (c4d.BaseList2D): The node containing the parameter. id (c4d.DescID): The DescID of the parameter to write. t_data (any): The parameter data. flags (int): The write flags. Returns: (bool, int): The outcome and the write flags. """ # If the parameter to write is our field list ... if id[0].id == c4d.ID_FIELDLIST: # Get the node's document and the node to reject. doc = node.GetDocument() nodeToReject = node[c4d.ID_REJECTION] or node # And ask FieldListContains() if nodeToReject is contained in # t_data. if PC13387.FieldListContains(doc, nodeToReject, t_data): # If so, signal that we did carry out the operation without # doing it, causing the old state to remain. return True, flags | c4d.DESCFLAGS_SET_PARAM_SET return True if __name__ == '__main__': c4d.plugins.RegisterObjectPlugin( id=PC13387.ID_PLUGIN, str="FieldList Rejection Workaround", g=PC13387, description="Opc13387", info=c4d.OBJECT_GENERATOR, icon=None)
-
This post is deleted! -
Hi @ferdinand! I've been messing around with your code and I think I have it sort of working as I need.
The problem I'm running into at the moment is that it would be possible to drag a Group Field into the plugin, and then as a separate action drag the plugin into that group, thereby still creating the feedback loop. So to prevent that I would need to check on the fields being used before I run my code, instead of when something is being dragged and dropped.
Is there a simpler way to handle this interaction? I'm worried that I might miss some case and it could cause an infintite loop of the plugin referencing itself via the fields.
Dan
-
Hello @d_schmidt,
well, you could just monitor that field-list and remove stuff in such case. But that can go south when whatever causes the crashes is invoked before you had the chance to sanitize the field-list; as you cannot hook directly into modifications of that group field. It also depends on the nature of your plugin. I would assume from the context that it is an
ObjectData
plugin which provides some form of 'geometry', i.e., overwritesGetVirtualObjects
orGetContour
. You could sanitize then the field-list whenGVO
orGC
is being called, but I could see this then causing all sorts of other problems, like for example double the amount of cache building.What I would do, is to crack down more on why your plugin is crashing. You did not provide much information on the nature of your plugin. To me it sounds a bit like you are trying to implement a deformer within a geometry providing plugin. Which works when the inputs for the deformation are just numeric values, but can cause problems when the inputs are scene dependent. Note that there is no geometry provided by us which does that, i.e., provides a fieldlist to deform itself (to my knowledge). What I would do, is to seperate the geometry part from the deformnation/modification part into seperate plugins. You can for example without problems deform a cube with a bend object which is driven by geometry field of the same cube. No extra steps required. But if you smush them together, you have to handle all the execution order yourself.
In the end this is all very speculative. Yes, you could also catch the case that an instance of your plugin is dragged into a group field which previously has been added to that instance's field list. But that will produce quite some overhead and other ifs and whens. I would try to conform more to how Cinema intends its scene to be organized. To get ehere any further, we will need more information on what your plugin does. And more information on the crashing.
Cheers,
Ferdinand -
Hi @ferdinand!
At the moment I'm using the Fields within GetContour. I want to use the field list at FIELDS to just get an output value, based on vectors I'm passing it. I'm using C4D Falloff to get the values. Then with the output value/weights I'm determining the length of my spline. from what I can tell it seems like the crash is occurring because my plugin is now referencing itself and continuously. The plugin changes, so the length of the spline changes from the field so the plugin changes, and so on.
This is my code super trimmed down:
GetContour(BaseObject * op, BaseDocument * doc, Float lod, BaseThread * bt ) { GeData data; const DescID fieldParameterID(FIELDS); maxon::BaseArray< maxon::BaseArray<Vector> > allpositions; if (!op->GetParameter(fieldParameterID, data, DESCFLAGS_GET::NONE)) { SplineObject* spline = SplineObject::Alloc(2, SPLINETYPE::LINEAR); return spline; } CustomDataType* const customData = data.GetCustomDataType(CUSTOMDATATYPE_FIELDLIST); FieldList* const fieldList = static_cast<FieldList*>(customData); if (!fieldList) { SplineObject* spline = SplineObject::Alloc(2, SPLINETYPE::LINEAR); return spline; } Int32 fieldListCount = 0; if (fieldList) fieldListCount = fieldList->GetCount(); Int32 pointcount =1; Float weight = 0; if (fieldListCount > 0) { maxon::BaseArray<maxon::Vector> positions; maxon::BaseArray<maxon::Vector> uvws; maxon::BaseArray<maxon::Vector> directions; resultVoid = positions.Resize(pointcount); resultVoid = uvws.Resize(pointcount); resultVoid = directions.Resize(pointcount); Vector pos = op->GetMg().off; FieldInput points(positions.GetFirst(), directions.GetFirst(), uvws.GetFirst(), 1, Matrix()); falloff->PreSample(doc, op, points, FIELDSAMPLE_FLAG::VALUE); falloff->Sample(pos, &weight, TRUE, 0.0, nullptr, 0); } SplineObject* spline = SplineObject::Alloc(2, SPLINETYPE::LINEAR); Vector* points = spline->GetPointW(); points[0] = Vector(0,100,0); points[0] = Vector(0,100,500*weight); return spline} //From GeGDescription cid = DescLevel(FIELDS, CUSTOMDATATYPE_FIELDLIST, 0); if (!singleid || cid.IsPartOf(*singleid, NULL)) { BaseContainer bc; bc = GetCustomDataTypeDefault(CUSTOMDATATYPE_FIELDLIST); bc.SetBool(DESC_FIELDLIST_NOCOLOR, TRUE); bc.SetBool(DESC_FIELDLIST_NODIRECTION, TRUE); bc.SetBool(DESC_FIELDLIST_NOROTATION, TRUE); BaseContainer acceptedObjects; acceptedObjects.InsData(ID_RKTRicochet, String());//matrix bc.SetString(DESC_NAME, "Field"_s); bc.SetBool(DESC_ANIMATE, true); if (!description->SetParameter(cid, bc, fieldgroup)) return TRUE; } //From Check Dirty GeData data; UInt32 falloffSum = 0; Bool superdirty = false; const DescID fieldParameterID(FIELDS); if (op->GetParameter(fieldParameterID, data, DESCFLAGS_GET::NONE)) { CustomDataType* const customData = data.GetCustomDataType(CUSTOMDATATYPE_FIELDLIST); FieldList* const fieldList = static_cast<FieldList*>(customData); //UInt32 falloffSum = 0; if (fieldList) { BaseContainer *dataInstance = op->GetDataInstance(); falloffSum = falloff->GetDirty(doc, dataInstance); } if (falloffSum != fieldDirty) { fieldDirty = falloffSum; dirty = TRUE; } }
Dan
-
Hello @d_schmidt,
thank you for providing more details and a rough break down. It is unfortunately the case that you are trying to do exactly what I feared you are doing, mixing up cache building and deformation. The culprit would be the field sampling part in
GetContour
.And your problem is not limited to the group field case where an instance of the hosting plugin node is fed into it after the group field has be added to hosting plugin node, but effectively applies anything that depends on the cache of your plugin instance, and which can be fed into a field list. Other examples would be all the object generators that take splines as inputs, e.g., the Lathe-, Loft- and Extrude object, the Connect, Instance- and the Symmetry object, and many other cases like for example the Spline Mask object. They all will cause this problem that when executing your
GetContour
, the cache of your plugin must be built, i.e.,GetContour
can be invoked, causing an infinite loop and stack overflow and therefore crash.You could of course ''just" sanity check the field list before you sample it in
GetContour
. But as demonstrated above, there are many cases to cover and you can never cover all of them, because there might be plugins out there which would be a case and you might not be aware of them. So, this "just" can quickly to into a giant headache. This goes effectively far beyond your initial question of refusing the instance of your plugin itself and is therefore not feasible.So, what would be reasonable patterns to handle this? As stated in my last posting, the recommend and effectively only viable approach is to separate these two operations, generation and deformation into their intended layers. As also already stated last time, you can easily construct a test case with Cinema's vanilla toolset to see that this will work without any extra steps reuired, because Cinema 4D will then handle the order of execution for you. Here the already mentioned example of a cube generator which is deforming itself, built out of the vanilla toolset:
When this is not an option, the only somewhat reasonable, but still not advisable option is to move the field list sampling out of
GetContour
. An alternative place could beNodeData::Message
. The idea then would be to evaluate the field-list into a cache and rely on these cached field-list sampling values withinGetContour
. But the details of this, onto which message to hook, i.e., the point when to build the sampling cache, its side-effects, like yourGetContour
being called, and how to constitute such field-list sampling cache (would have to be some lattice data structure) all fall out of scope of support, because you do violate here one of the major design principles of the SDK.I do understand that your case is somewhat special, and it might be hard to restate your "deformation" as a deformer. I think you are working on the rocket-lasso ray-casting ping-pong spline thing, right? So, in the end you might have to take the hard route of trying to sanitize the field-lists before you sample them, but at least I would REALLY make sure that this is my only option, because there will be no shortage of problems you encounter with this and we, the SDK-Team, can only offer very limited support for this, due to it being out of scope of support.
Thank you for your understanding,
Ferdinand -
Hi @ferdinand , thank you for the impressive write up!
I'm not the most familiar with Deformers, so I have a couple of questions on that front. This does seem out of scope of the initial question, but I'm not sure if I should make a different thread or not.
With your cube example: This works because the Bulge deformer is looking at Cache of the Cube and then returning the Deform Cache of it, right? So there is no infinite loop because it isn't modify the Deform Cache.
Is a Deformer capable of doing what I need it to in this case? My understanding is that a Deformer generally moves points around and the like. What I want is to get a float value from the Field to then generator a different cache with that value in GetContour. This wouldn't be just moving points around, but creating completely new ones or utterly changes existing ones.
This seems like it would have the same problem, since my main plugin wouldn't be modified, but recalculating based on changes from the Deformer. So the Cache would rebuild which would trigger infinite rebuilds as a result.
Dan
-
Hello @d_schmidt,
@d_schmidt said in Field Refusing Object:
This does seem out of scope of the initial question, but I'm not sure if I should make a different thread or not.
Technically true, but we are fine here.
With your cube example: This works because the Bulge deformer is looking at Cache of the Cube and then returning the Deform Cache of it, right? So there is no infinite loop because it isn't modify the Deform Cache.
Yes, that is mostly what is happening. The problem your plugin has, is that the generation of its cache can depend on its own cache. Deformers are a common pattern to avoid such cyclic definitions. There can be cases where they do not fully solve this, i.e., where you then have a ping-pong case of when the deformer is constantly reevaluated, i.e., can bring Cinema 4D to a crawl, but you will not produce stack overflows, i.e., crashes. My bulge deformer case is such an example of constant reevaluation. When you set the sampling mode of the point layer of the cube to volume in the layer tab and then set cube subdivisions to something like 100³, it will bring Cinema to a crawl, because it will constantly chew on reevaluating the cube. This happens of course constantly, but for lightweight to medium sized deformations it will not be noticeable. And just to be clear: This only happens when you have these cyclic definitions.
Is a Deformer capable of doing what I need it to in this case? My understanding is that a Deformer generally moves points around and the like. What I want is to get a float value from the Field to then generator a different cache with that value in GetContour. This wouldn't be just moving points around but creating completely new ones or utterly changes existing ones.
Yes, deformers are mostly meant for moving stuff around, but you can also modify the point or polygon count of things. The bevel deformer does that for example. At the end of this posting, you will find an extension of the previously posted Python example which doubles the vertex count of an input spline and adds a bit of jitter to the added intermediate points. The plugin is now a deformer
ObjectData
which is close to what you are trying to do.This seems like it would have the same problem, since my main plugin wouldn't be modified, but recalculating based on changes from the Deformer. So, the Cache would rebuild which would trigger infinite rebuilds as a result.
As stated above, yes, the problem remains, although you avoid causing stack overflows and therefor crashes. In the end this is an unsolvable riddle. A cyclic definition is a cyclic definition. When your cache depends on building your cache, you have a problem. One approach is to sanity check everything that moves for not being a self-reference. This is of course labor intensive to do. Splitting things up shifts the burden to Cinema at the cost of having a sluggish scene when the user overdoes it.
Cheers,
FerdinandThe effect of the deformer, in red the deformed spline adding points in between, and in white a copy of the input spline:
The code (mostly the same as above, I only added
ModifyObjects
, the two hash functions and changed the registration call, so that the plugin is now a deformer):"""Example for emulating refusing a field layer in field list with NodeData::SetParameter. As discussed in: https://developers.maxon.net/forum/topic/13387/ """ import c4d import math import struct # In the C++ you do not have to do these hashing gymnastics I do here, # because Cinema does provide the Random class there. Python's random is # however ill suited for this task, which is why I am providing my own # hash functions here. def Hash11(x, seed=1234., magic=(1234.5678, 98765.4321)): """Returns a pseudo random floating-point hash in the interval [-1, 1]. Args: x (float): The value to get the pseudo random hash for. seed (float, optional): The seed offset for the hash. magic (tuple[float, float], optional): The magic numbers. Defaults to (1234.5678, 98765.4321). Returns: float: The random hash for x in the interval [-1, 1]. """ return math.modf(math.sin((x * seed) * magic[0]) * magic[1])[0] def Hash13(x, seed=1234., magic=(1234.5678, 98765.4321)): """Returns a pseudo vector floating-point hash in the interval [-1, 1]. Wraps around Hash11. Args: as Hash11. Returns: c4d.Vector: The random hash for x in the interval [-1, 1]. """ xc = Hash11(x, seed, magic) yc = Hash11(x * xc, seed, magic) zc = Hash11(x * yc, seed, magic) return c4d.Vector(xc, yc, zc) class PC13387(c4d.plugins.ObjectData): """Example ObjectData plugin that has two parameters. * ID_REJECTION - A base-link for an object to reject. * ID_FIELDLIST - A field list which could contain a a field layer for that object to reject and which we want to prevent. """ ID_PLUGIN = 1057375 @staticmethod def FieldListContains(doc, testNode, fieldList): """Determines if a node is contained in a field list or not. Iterating over field lists is not trivial, and I do not have the time to cover here any corner case, feel free to ask questions once you get stuck. Important to understand is also that a field list does contain field layers which are not equal to their object manager representation. The object manager repr. of a field layer has to be retrieved with GetLinkedObject() if there is one (which is not a given). Args: doc (c4d.documents.BaseDocument): The document the field list is contained in. testNode (c4d.GeListNode): The node to test for. fieldList (c4d.FieldList): The field list to test. Returns: bool: If testNode is contained or not. """ def getFieldLayers(node): """ """ visited = [] end = node.GetUp() if isinstance(node, c4d.GeListNode) else None while node: # If the current node is a newly encountered one. if node not in visited: visited.append(node) # When it is a field layer, yield it and deal with group # fields ... if isinstance(node, c4d.modules.mograph.FieldLayer): yield node # For the special case of a group field, we have to unpack # the data of the field by accessing its content via its # object manager representation. Since what one does see # in the field list, i.e., fields below an group field, is # actually not there, but only a view. The data is stored # with the field list of the group field. So we cannot # access this data by iterating over the field tree we # are currently iterating over, i.e., # myGroupFieldLayer.GetDown() will yield None. # Get the object manger representation of the current # field layer. If there is one and its type is that of # a field group: linkedNode = node.GetLinkedObject(doc) if linkedNode and linkedNode.CheckType(c4d.Fgroup): # Get the field list and iterate over it. fieldList = linkedNode[c4d.FIELDGROUP_FIELDS] root = fieldList.GetLayersRoot() for nestedNode in getFieldLayers(root): yield nestedNode # There are more special cases in the field list hierarchy, # e.g., folders, which also have to be treated specifically. # I did not do that here. # Normal depth-first traversal of a node tree if node.GetDown() and node.GetDown() not in visited: node = node.GetDown() elif node.GetNext(): node = node.GetNext() else: node = node.GetUp() if node == end: return # Get the root layer of the field list and traverse it. root = fieldList.GetLayersRoot() for fieldLayer in getFieldLayers(root): # When the object linked to the field layer is equal to the one # we want to reject, then we return True, as we found it. linkedNode = fieldLayer.GetLinkedObject(doc) if linkedNode == testNode: return True return False def Init(self, node): """Not documented. """ self.InitAttr(node, c4d.BaseList2D, c4d.ID_REJECTION) self.InitAttr(node, c4d.FieldList, c4d.ID_FIELDLIST) return True def SetDParameter(self, node, id, t_data, flags): """Overwrite the process of writing a parameter. When a field list is being modified, e.g., by a drag and drop event, we validate that to be written field list by iterating over it. When we encounter the object we want to reject, we signal that we did carry out the writing of the parameter without doing it, causing the old state of the field list to remain, i.e., the drag and dropped object has been rejected. Args: node (c4d.BaseList2D): The node containing the parameter. id (c4d.DescID): The DescID of the parameter to write. t_data (any): The parameter data. flags (int): The write flags. Returns: (bool, int): The outcome and the write flags. """ # If the parameter to write is our field list ... if id[0].id == c4d.ID_FIELDLIST: # Get the node's document and the node to reject. doc = node.GetDocument() nodeToReject = node[c4d.ID_REJECTION] or node # And ask FieldListContains() if nodeToReject is contained in # t_data. if PC13387.FieldListContains(doc, nodeToReject, t_data): # If so, signal that we did carry out the operation without # doing it, causing the old state to remain. return True, flags | c4d.DESCFLAGS_SET_PARAM_SET return True def ModifyObject(self, mod, doc, op, op_mg, mod_mg, lod, flags, thread): """ """ # Get out when the input object is not a line object. if not isinstance(op, c4d.LineObject): print (f"Illegal modifier input: {op}") return False # And get out when the input object has less than 2 points. pcnt = op.GetPointCount() if pcnt <= 1: print (f"Invalid spline input: {op}") return False # Now we go over all points and just subdivide the line segments by # calculating an intermediate point. points = op.GetAllPoints() newPoints = [] for i in range(1, pcnt - 1): # A line segment. o, q = points[i - 1], points[i] # The interpolated point in between with a little bit of jitter # to make it a bit more exciting to look at. p = (o + q) * .5 + Hash13(float(i)) * 25 newPoints += [o, p] # Resize the line object. newCnt = len(newPoints) print ("newCnt:", newCnt, op.GetSegmentCount()) op.ResizeObject(newCnt) # Write the new point information. op.SetAllPoints(newPoints) # One problem with this is that the underlying segment data is not # being updated. I am not sure if this is just a Python thing, or if # you will encounter this also in C++. The segment count of the tag # is still the old value, which will cause the returned line object # to have the correct point count, but rendered will only be the old # number of segments. So, we are going to fix this. In C++ there are # easier ways to do this, in Python we have to use raw memory access. # But you can also use this approach in C++ if you want to. segmentTag = op.GetTag(c4d.Tsegment) if segmentTag is None: return False data = segmentTag.GetLowlevelDataAddressW() # Unpack the first 4 bytes, the segment count, a segment is just this: # # struct Segment # { # Int32 cnt; # Bool closed; # }; cnt = struct.unpack("i", data[:4]) print ("Segment count:", cnt) # Write the new segment count. data[:4] = struct.pack("i", newCnt) # This will get a bit more complicated when there are multiple Segment # instances, I kept things simple here for a single segment case. # Now we are good to go. return True if __name__ == '__main__': c4d.plugins.RegisterObjectPlugin( id=PC13387.ID_PLUGIN, str="Spline Deformer", g=PC13387, description="Opc13387", info=c4d.OBJECT_MODIFIER, icon=None)
-
Hi @ferdinand !
Thank you for all the help! I think that wraps up my question, not the answer I wanted, but it all makes sense and I know a bit more about what how Fields work.
I'll look into ModifyObjects more as well.
Dan
-
Hello Dan,
I know that the situation is a bit unsatisfying, but that is unfortunately mostly a design problem you have there and not an API one. Which are often questions that are hard to answer "in a good way" and technically also out of scope of support.
In case you encounter any problems further down the road, please do not hesitate to revisit us.
Cheers,
Ferdinand -
-