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()