-
Posts
15 -
Joined
-
Last visited
-
Days Won
1
Community Answers
-
zipit's post in A way to search Xpresso nodes? was marked as the answer
Hi @Robert Krawczyk,
node iteration is a bit of a hornet's nest in the Cinema 4D SDK, because although it might look like just a "trivial" graph walk, it can become quite complex due to all the corner cases that lurk in the depth of how Cinema 4D does organize its scene graph. And graph walks are also often not trivial in itself when one has to meet certain conditions like starting at an arbitrary point which cannot be "overshot" or having crazy requirements like not producing stack overflows 😉 We are aware of the problem at the SDK-Team and have it on our bucket list, i.e., want to provide an interface for it at some point. But for now, it must be done manually.
Your problem has two components: Finding all Xpresso tags in a scene and iterating over all their nodes. For the latter GetDown()/Next() were the correct approach in principal, but the devil is in the detail here, one can get easily lost in these graphs. The solution provided by @JED has certainly its strengths in its simplicity, but it will not yield any nodes that are within a group node. The solution provided below uses a more abstract approach, which allows one to use it for example to also retrieve all the Xpresso tags in the scene. In practice it would be nice if we already had an node iteration interface in the SDK for the classic API, but currently we do not. My example provides a simple version, tailored to the specific case. Which can be treated as a black box if one feels uncomfortable with the code. The core logic of what you want to do, is then only this relatively accessible bit of code:
# We iterate over all Xpresso tags in the document. We pass in the first # object in the scene and specify that we are only interested in nodes of # type c4d.Texpresso (Xpresso tags). for xpressoTag in NodeIterator(doc.GetFirstObject(), c4d.Texpresso): # Print out some stuff about the currently yielded Xpresso tag. name, nid = xpressoTag.GetName(), id(xpressoTag) print(f"The Xpresso tag {name} at {nid} has the nodes:") # Get its master node and root node in the Xpresso graph. masterNode = xpressoTag.GetNodeMaster() root = masterNode.GetRoot() # And iterate over all nodes in that root node. for xpressoNode in NodeIterator(root): print(f"\t{xpressoNode}")
If ran on a scene, it looks like this:
Cheers,
Ferdinand
The full code:
"""Example for iterating over all Xpresso nodes in a scene. Node iteration can be quite a complex topic in Cinema 4D due to the many graph relations that are contained in a document and the problems that arise from recursive solutions for the problem - stack overflows or in Python the safety measures of Python preventing them. We have this topic on our bucket list in the SDK-Team, but for now one has to approach it with self-provided solutions. This is an example pattern for a node iterator (which does not take caches into account). One can throw it at any kind of GeListNode and it will yield in a stack-safe manner all next-siblings and descendants of a node. How this iteration is carried out (depth or breadth first), to include any kind of siblings, not just next-siblings, to include also ancestors and many things more could be done differently. This pattern is tailored relatively closely to what the topic demanded. A more versatile solution would have to be provided in the SDK. """ import c4d # This is a node iteration implementation. To a certain degree this can be # treated as a black box and does not have to be understood. This will # however change when one wants to include other use-case scenarios for which # one would have to modify it. def NodeIterator(node, types=None): """An iterator for a GeListNode node graph with optional filtering. Will yield all downward siblings and all descendants of a node. Will not yield any ancestors of the node. Is stack overflow (prevention) safe due to being truly iterative. Alsob provides a filter mechanism which accepts an ineteger type symbol filter for the yielded nodes. Args: node (c4d.GeListNode): The starting node for which to yield all next- siblings and descendants. types (None | int | tuple[int]), optional): The optional type filter. If None, all nodes will bey yielded. If not None, only nodes that inherit from at least one of the type symbols defined in the filter tuple will be yielded. Defaults to None. Yields: c4d.GeListNode: A node in the iteration sequence. Raises: TypeError: On argument type oopses. """ # Some argument massaging and validation. if not isinstance(node, c4d.GeListNode): msg = "Expected a GeListNode or derived class, got: {0}" raise TypeError(msg.format(node.__class__.__name__)) if isinstance(types, int): types = (types, ) if not isinstance(types, (tuple, list, type(None))): msg = "Expected a tuple, list or None, got: {0}" raise TypeError(msg.format(types.__class__.__name__)) def iterate(node, types=None): """The iteration function, walking a graph depth first. Args: same as outer function. Yields: same as outer function. """ # This or specifically the usage of the lookup later is not ideal # performance-wise. But without making this super-complicated, this # is the best solution I see here (as we otherwise would end up with # three versions of what the code does below). visisted = [] terminal = node.GetUp() if isinstance(node, c4d.GeListNode) else None while node: if node not in visisted: if types is None or any([node.IsInstanceOf(t) for t in types]): yield node visisted.append(node) if node.GetDown() and node.GetDown() not in visisted: node = node.GetDown() elif node.GetNext(): node = node.GetNext() else: node = node.GetUp() if node == terminal: break # For each next-sibling or descendant of the node passed to this call: for iterant in iterate(node): # Yield it if it does match our type filter. if types is None or any([iterant.IsInstanceOf(t) for t in types]): yield iterant # And iterate its tags if it is BaseObject. if isinstance(iterant, c4d.BaseObject): for tag in iterate(iterant.GetFirstTag(), types): yield tag def main(): """Entry point. """ # We iterate over all Xpresso tags in the document. We pass in the first # object in the scene and specify that we are only interested in nodes of # type c4d.Texpresso (Xpresso tags). for xpressoTag in NodeIterator(doc.GetFirstObject(), c4d.Texpresso): # Print out some stuff about the currently yielded Xpresso tag. name, nid = xpressoTag.GetName(), id(xpressoTag) print(f"The Xpresso tag {name} at {nid} has the nodes:") # Get its master node and root node in the Xpresso graph. masterNode = xpressoTag.GetNodeMaster() root = masterNode.GetRoot() # And iterate over all nodes in that root node. for xpressoNode in NodeIterator(root): print(f"\t{xpressoNode}") if __name__ == '__main__': main()
-
zipit's post in Flatten polygon selection based on normals was marked as the answer
Hi @bjlotus, hi @Cairyn,
there is one problem with your normals calculation. You calculate just a vertex normal in the polygon and then declare it to be the polygon normal 😉 (your variable "cross"), i.e., you assume all your polygons to be coplanar. With today's high density meshes you can probably get away with that to a certain degree, but it would not hurt to actually calculate the mean vertex normal of a polygon, a.k.a., the polygon normal to have a precise result.
The problem with your "flattening" is that it is not one. Unless I have overread something here in the thread, the missing keyword would be a point-plane projection. You just translate all selected points by a fixed amount, which was if I understood that correctly only a work in progress, but obviously won't work.
Things could also be written a bit more tidely and compact in a pythonic fashion, but that has very little impact on the performance and is mostly me being nitpicky 😉 I did attach a version of how I would do this at the end (there are of course many ways to do this, but maybe it will help you).
Cheers,
Ferdinand
"""'Flattens' the active polygon selection of a PolygonObject. Projects the points which are part of the active polygon selection into the mean plane of the polygon selection. """ import c4d def GetMean(collection): """Returns the mean value of collection. In Python 3.4+ we could also use statistics.mean() instead. Args: collection (iterable): An iterable of types that support addition, whose product supports multiplication. Returns: any: The mean value of collection. """ return sum(collection) * (1. / len(collection)) def GetPolygonNormal(cpoly, points): """Returns the mean of all vertex normals of a polygon. You could also use PolygonObject.CreatePhongNormals, in case you expect to always have a phong tag present and want to respect phong breaks. Args: cpoly (c4d.Cpolygon): A polygon. points (list[c4d.vector]): All the points of the object. Returns: c4d.Vector: The polygon normal. """ # The points in question. a, b, c, d = (points[cpoly.a], points[cpoly.b], points[cpoly.c], points[cpoly.d]) points = [a, b, c] if c == d else [a, b, c, d] step = len(points) - 1 # We now could do some mathematical gymnastics to figure out just two # vertices we want to use to compute the normal of the two triangles in # the quad. But this would not only be harder to read, but also most # likely slower. So we are going to be 'lazy' and just compute all vertex # normals in the polygon and then compute the mean value for them. normals = [] for i in range(step + 1): o = points[i - 1] if i > 0 else points[step] p = points[i] q = points[i + 1] if i < step else points[0] # The modulo operator is the cross product. normals.append(((p - q)) % (p - o)) # Return the normalized (with the inversion operator) mean of them. return ~GetMean(normals) def ProjectOnPlane(p, q, normal): """Projects p into the plane defined by q and normal. Args: p (c4d.Vector): The point to project. q (c4d.Vector): A point in the plane. normal (c4d.Vector): The normal of the plane (expected to be a unit vector). Returns: c4d.Vector: The projected point. """ # The distance from p to the plane. distance = (p - q) * normal # Return p minus that distance. return p - normal * distance def FlattenPolygonSelection(node): """'Flattens' the active polygon selection of a PolygonObject. Projects the points which are part of the active polygon selection into the mean plane of the polygon selection. Args: node (c4d.PolygonObject): The polygon node. Returns: bool: If the operation has been carried out or not. Raises: TypeError: When node is not a c4d.PolygonObject. """ if not isinstance(op, c4d.PolygonObject): raise TypeError("Expected a PolygonObject for 'node'.") # Get the point, polygons and polygon selection of the node. points = node.GetAllPoints() polygons = node.GetAllPolygons() polygonCount = len(polygons) baseSelect = node.GetPolygonS() # This is a list of booleans, e.g., for a PolygonObject with three # polygons and the first and third polygon being selected, it would be # [True, False, True]. polygonSelection = baseSelect.GetAll(polygonCount) # The selected polygons and the points which are part of these polygons. selectedPolygonIds = [i for i, v in enumerate(polygonSelection) if v] selectedPolygons = [polygons[i] for i in selectedPolygonIds] selectedPointIds = list({p for cpoly in selectedPolygons for p in [cpoly.a, cpoly.b, cpoly.c, cpoly.d]}) selectedPoints = [points[i] for i in selectedPointIds] # There is nothing to do for us here. if not polygonCount or not selectedPolygons: return False # The polygon normals, the mean normal and the mean point. The mean point # and the mean normal define the plane we have to project into. Your # image implied picking the bottom plane of the bounding box of the # selected vertices as the origin of the plane, you would have to do that # yourself. Not that hard to do, but I wanted to keep things short ;) polygonNormals = [GetPolygonNormal(polygons[pid], points) for pid in selectedPolygonIds] meanNormal = ~GetMean(polygonNormals) meanPoint = GetMean(selectedPoints) # Project all the selected points. for pid in selectedPointIds: points[pid] = ProjectOnPlane(points[pid], meanPoint, meanNormal) # Create an undo, write the points back into the polygon node and tell # it that we did modify it. doc.StartUndo() doc.AddUndo(c4d.UNDOTYPE_CHANGE, node) node.SetAllPoints(points) doc.EndUndo() node.Message(c4d.MSG_UPDATE) # Things went without any major hiccups :) return True def main(): """Entry point. """ if FlattenPolygonSelection(op): c4d.EventAdd() if __name__ == '__main__': main()