Knife in pyhon

I’m rather inexperienced with Python in houdini, so I didn’t bother with external files and a clean workflow for this one.

The knife tool bothered my because when cutting a single face it would separate the face, not cutting shared edges on other faces. Hence I wrote this to find faces sharing the cut edge and rebuild those faces too.

A bit of novice info first
Use File -> New Operator Type, set Style to Python and Network Type to Geometry, save it into an OTL and create the node. Then right click the node and at the bottom select Type properties. There add the following parameters under the Parameters tab:
int, Face nr (id)
float3, Origin (origin)
float, Distance (dist)
float3, Direction (dir)
Then go to the Code tab to start writing code.

Not knowing how to make a param that accepts a group or a pattern it currently only can cut one face, determined by the ‘Face nr’ parameter.

Writing the code
First I’ll need to know the parameter contents, so the basic setup of the node looks like this:

node = hou.pwd()
geo = node.geometry()
### Parse parameters ###
target = node.evalParm("id")
origin = hou.Vector3( node.evalParm("originx"), node.evalParm("originy"), node.evalParm("originz") )
dist = node.evalParm("dist")
dir = hou.Vector3( node.evalParm("dirx"), node.evalParm("diry"), node.evalParm("dirz") )

Then, matching the original Knife operator, I added a distance parameter, normally this is senseless because the distance simply moves the cutting plane’s origin into the direction vector, by that distance. Safely done by normalizing first, like this:

dir = dir.normalized()
origin += dir*dist

Ray – plane intersection
The next thing is to cut the target face where it intersects with the given plane. For this I raycast every edge of the face through the plane, any intersections found that still lie on the edge should become an inbetween point, splitting the edge in two. So first the raycasting part:

def rayPlaneIntersect(rayorigin, in_raydirection, planeorigin, in_planenormal):
    '''
    @returns: Vector3, intersectionPoint-rayOrigin
    '''
    raydirection = in_raydirection.normalized()
    planenormal = in_planenormal.normalized()
    distanceToPlane = (rayorigin-planeorigin).dot(planenormal)
    triangleHeight = raydirection.dot(-planenormal)
    if not distanceToPlane:
        return rayorigin-planeorigin
    if not triangleHeight:
        return None #ray is parallel to plane
    return raydirection * distanceToPlane * (1.0/triangleHeight)

It essentially dots the ray origin to the plane normal to find out the distance to the plane, then the point on the plane and the ray origin form one line with a known length, trigonometry will determine the third point for we know its direction (the ray direction) and only need to determine the length. More on this here.

Cutting the face
To cut the face I will extract the primitive with the given id (stored in target), then I’ll iterate over it’s vertices to define the edges and check for every edge if it should be split, if so I append the new point to both the polygon and the edge for later usage.

By dotting the intersection point vector with the ray (edge) direction I know the parameter, which should remain between 0 and the edgelength to still be on the edge.

### Cut target ###
verts = geo.iterPrims()[target].vertices()
nverts = len(verts)
edges = []
for i in range(nverts):
    edges.append( [verts[i].point(), verts[(i+1)%nverts].point()] )
    edgedirection = edges[-1][1].position()-edges[-1][0].position()
    intersectpt = rayPlaneIntersect(edges[-1][0].position(), edgedirection, origin, dir)
    if intersectpt == edges[-1][0].position()-origin:
        continue
    if not intersectpt: #edge is parallel to cutting plane
        continue
    param = intersectpt.dot(edgedirection.normalized())
    if param > 0 and param < edgedirection.length():
        pt = geo.createPoint()
        pt.setPosition( intersectpt+edges[-1][0].position() )
        edges[-1].append( pt )

Next is to create the split geometry, after that we'll have a replica of the knife tool working on just one face. For this I start adding vertices to the first polygon until a cut edge is reached, then I add a new polygon and start adding vertices to that. Beforehand I work back to the last cut so I don't put half of the first polygon (the points between the last cut and point 0) in another polygon.

### Create output polygon(s) ###
polys = [geo.createPolygon()]
#find last cut point for wrapping the first polygon
lastcut = None
for i in range(len(edges)-1,-1,-1):
    if len(edges[i]) > 2:
        lastcut = i
        break;

#iterate over edges to build new polygons
wrap = False
for i in range(len(edges)):
    if wrap:
        polys[0].addVertex(edges[i][0])
        continue
    polys[-1].addVertex(edges[i][0])
    if len(edges[i]) > 2:
        polys[-1].addVertex(edges[i][2])
        if i == lastcut: #wrap to first polygon at last cut
            wrap = True
            polys[0].addVertex(edges[i][2])
            continue
        polys.append(geo.createPolygon())
        polys[-1].addVertex(edges[i][2])

Getting polygons from an edge
Given two point numbers and a geometry object we iterate over all primitives' vertices, if any adjacent vertices (thus an edge) matches the given ids it shares this edge.

Error warning
Also I ran into an interesting issue, where storing len(verts) before the for loop to speed it up (not calling it during the module but using the variable instead) it didn't contain a valid number, resulting in i exceeding len(verts).

def getPolygonsWithEdge(geom, edgeids):
    '''
    @param geo: hou.Geometry, geometry to search
    @param edgeids: tuple of 2 ints, point numbers
    describing the edge to find shared faces for

    @returns: list of hou.Prim, all primitives sharing this edge

    if the points are not connected by an edge
    (adjacent in the vertex list of any primitive)
    the result is an empty list
    '''
    out = []
    for poly in geom.prims():
        verts = poly.vertices()
        for i in range(len(verts)):
            if verts[i].point().number() in edgeids and\
               verts[(i+1)%len(verts)].point().number() in edgeids:
                out.append(poly)
    return out

The last bit of code
Now to make this tool actually renewing I need to find any polgons sharing the renewed edges and rebuild them to include the new points as well. I simply go over the points and if I encounter the cut edge I insert the additional point before continuing. Here I also track the old primitives to delete at the end.

### Find polygons sharing the cut edges ###
deleteprims = [geo.iterPrims()[target]]

for i in range(len(edges)):
    if len(edges[i]) > 2:
        #rebuild polygons that share the cut edge
        edgeids = (edges[i][0].number(), edges[i][1].number())
        sharedprims = getPolygonsWithEdge(geo, edgeids)
        for prim in sharedprims:
            deleteprims.append(prim)
            split = False
            if prim.number() != target: #ignore the polygon we are cutting entirely
                poly = geo.createPolygon()
                for vert in prim.vertices():
                    poly.addVertex(vert.point())
                    if vert.point().number() in edgeids and split == False:
                        split = True
                        poly.addVertex(edges[i][2])

geo.deletePrims(deleteprims,True)

Here's the full code again, two issues remain, being the changing of prim numbers (and groups containing those prims will lose them, even if they're the adjacent faces) and concave faces get overlapping primitives that should actually be joined (looking at the knife tool).

If you draw a closed polygonal curve with these coordinates and cut it with both the knife and this tool the issue will become clear:
-2,1,0 -1,1,0 -0.5,-0.25,0 0.5,-0.25,0 1,1,0 2,1,0 2,-1,0 0,-2,0 -2,-1,0

'''
@todo: fix concave face errors
@todo: insert prim with right number (important on adjacent faces, optional on new cut faces); also maintain groups!
'''
node = hou.pwd()
geo = node.geometry()


def rayPlaneIntersect(rayorigin, in_raydirection, planeorigin, in_planenormal):
    '''
    @returns: Vector3, intersectionPoint-rayOrigin
    '''
    raydirection = in_raydirection.normalized()
    planenormal = in_planenormal.normalized()
    distanceToPlane = (rayorigin-planeorigin).dot(planenormal)
    triangleHeight = raydirection.dot(-planenormal)
    if not distanceToPlane:
        return rayorigin-planeorigin
    if not triangleHeight:
        return None #ray is parallel to plane
    return raydirection * distanceToPlane * (1.0/triangleHeight)


def getPolygonsWithEdge(geom, edgeids):
    '''
    @param geo: hou.Geometry, geometry to search
    @param edgeids: tuple of 2 ints, point numbers
    describing the edge to find shared faces for

    @returns: list of hou.Prim, all primitives sharing this edge

    if the points are not connected by an edge
    (adjacent in the vertex list of any primitive)
    the result is an empty list
    '''
    out = []
    for poly in geom.prims():
        verts = poly.vertices()
        for i in range(len(verts)):
            if verts[i].point().number() in edgeids and\
               verts[(i+1)%len(verts)].point().number() in edgeids:
                out.append(poly)
    return out


### Parse parameters ###
target = node.evalParm("id")
origin = hou.Vector3( node.evalParm("originx"), node.evalParm("originy"), node.evalParm("originz") )
dist = node.evalParm("dist")
dir = hou.Vector3( node.evalParm("dirx"), node.evalParm("diry"), node.evalParm("dirz") )
dir = dir.normalized()
origin += dir*dist

### Cut target ###
verts = geo.iterPrims()[target].vertices()
nverts = len(verts)
edges = []
for i in range(nverts):
    edges.append( [verts[i].point(), verts[(i+1)%nverts].point()] )
    edgedirection = edges[-1][1].position()-edges[-1][0].position()
    intersectpt = rayPlaneIntersect(edges[-1][0].position(), edgedirection, origin, dir)
    if intersectpt == edges[-1][0].position()-origin:
        continue
    if not intersectpt: #edge is parallel to cutting plane
        continue
    param = intersectpt.dot(edgedirection.normalized())
    if param > 0 and param < edgedirection.length():
        pt = geo.createPoint()
        pt.setPosition( intersectpt+edges[-1][0].position() )
        edges[-1].append( pt )

### Create output polygon(s) ###
polys = [geo.createPolygon()]
#find last cut point for wrapping the first polygon
lastcut = None
for i in range(len(edges)-1,-1,-1):
    if len(edges[i]) > 2:
        lastcut = i
        break;

#iterate over edges to build new polygons
wrap = False
for i in range(len(edges)):
    if wrap:
        polys[0].addVertex(edges[i][0])
        continue
    polys[-1].addVertex(edges[i][0])
    if len(edges[i]) > 2:
        polys[-1].addVertex(edges[i][2])
        if i == lastcut: #wrap to first polygon at last cut
            wrap = True
            polys[0].addVertex(edges[i][2])
            continue
        polys.append(geo.createPolygon())
        polys[-1].addVertex(edges[i][2])

### Find polygons sharing the cut edges ###
deleteprims = [geo.iterPrims()[target]]

for i in range(len(edges)):
    if len(edges[i]) > 2:
        #rebuild polygons that share the cut edge
        edgeids = (edges[i][0].number(), edges[i][1].number())
        sharedprims = getPolygonsWithEdge(geo, edgeids)
        for prim in sharedprims:
            deleteprims.append(prim)
            split = False
            if prim.number() != target: #ignore the polygon we are cutting entirely
                poly = geo.createPolygon()
                for vert in prim.vertices():
                    poly.addVertex(vert.point())
                    if vert.point().number() in edgeids and split == False:
                        split = True
                        poly.addVertex(edges[i][2])

geo.deletePrims(deleteprims,True)

Leave a Reply

Your email address will not be published. Required fields are marked *