Automatically removing holes (circle) from surface

Hi,
I was wondering, is there any easy way automatically remove holes like in the picture, that are circles? Maybe there is ready-made tool for this?
My other approach to this problem was to code my own tool. Would have find vertexes that make 2D (flat) circle and then collapse edges and surfaces.
Just have to define circle first…

It will be easy to automatically remove sets of faces that are identical to each other. It will be much harder to define a standard of “what should be a hole” and get that to work consistently. Assuming you’re using good topology, there’s not much difference between a hole and everything else, as a “hole” is just four quads that share four edges:


So I think you’d need to be able to recognize the ring of quads around the hole as well

1 Like

For 2D planes you can do select non manifold and have the holes selected automatically. However if you have a 3D hole (from a 3D extruded plane) you can’t do anything other than finding a very complex algorithm that does fuzzy mathing based on an initial template.

1 Like

if I had to do this I would probably start by creating a list of all closed face loops and calculate the total area of all faces in that loop, anything below a certain threshold would be considered a ‘hole’. You could avoid false positives by throwing out loops that don’t have approximately equal sized faces. If you’re working with very consistent geometry and already know how many segments your holes have, you could throw out any results that don’t have that exact number of faces. Once you’ve found the loops that comprise the hole, it’s just a matter of deleting them and then filling the resulting boundaries.

Another way to do it would be to get a list of all closed face loops, and then calculate the angle of each boundary edge to determine if it’s a circle. If all of the angles are within an appropriate threshold you’ve got yourself a circle (or rather, a polygon with equal sides). ie) if you have a ‘square’ hole: 360º / 4 = 90º, you’d expect all of the angles to be within a very small amount of that. a 16 sided ‘circle’ would have an angle of 22.5º between each edge, etc.

1 Like

True, defining the hole would not be easy task. What about if a select one of the holes and it would select all the similar vertex combinations from the mesh. That would need some interesting math. Or those blender have that kind of tool out of the box?

Or let’s make it even easier, if I just select one of the holes ( like in the first picture), is there easy way to “remove” that hole clean way? The result would be that in the end, there is just two clean faces. Now if I have to remove the hole, I have to first select upper vertices and merge them and then select the lower vertices and merge them and in the end the two remaining vertices have edge between them ( no need for that)
The closed faces loops sound actually good approach. I was trying to do the same with vertices, but it cannot find all of them, if the vertex is connected to somewhere else also. The face loops wound work if the faces are triangulated, but that can be removed with limited dissolve too.

Made something like this. It is werry raff solution now and all the debug prints are in. Also the mesh has to be triangluated to work

import bpy
import bmesh

from mathutils import Vector
from itertools import combinations
import random
import time
import os
os.system("cls")

def round_vector( vector, decimals ):
    # returns a tuple so that we avoid to allocate a new vector
    return (round(vector.x, decimals), round(vector.y, decimals), round(vector.z, decimals))


def find_plane( points, precision_decimals = 4 ):
    if type(points) is set:
        points = list(points)
    best_plane = set()
    planes = {} #One allocation for the set and the set is now a raw Python set
    for a in range( len(points) - 2 ):
        planes.clear() #Clear instead of allocating again
        pa = points[a]
        for b in range( a + 1, len(points) - 1 ): #Two loops instead of combinations
            pb = points[b] #Get once for all 'c'
            ab = pb.co - pa.co #Calculated once for all 'c'
            for c in range( b + 1, len(points) ):
                pc = points[c]
                ac = pc.co - pa.co
                normal = round_vector( ab.cross(ac).normalized(), precision_decimals ) #No freeze needed as we have a tuple
                plane = planes.get( normal )
                if not plane:
                    plane = {pa} #Immediately allocated raw set structure
                    planes[normal] = plane
                plane.add(pb)
                plane.add(pc)
                if len(best_plane) < len(plane): #Avoids if else assignation
                    best_plane = plane

    return best_plane
def get_selected_vert(bm):
    return [vertex for vertex in bm.verts if vertex.select]

def get_same_edge(vert1,vert2):
    for edge1 in vert1.link_edges:
        for edge2 in vert2.link_edges:
            if edge2 == edge1:
                return edge1

def remove_circle():
    me = bpy.context.edit_object.data
    bm = bmesh.from_edit_mesh(me)
    points = get_selected_vert(bm)
    print(len(points))
    plane = find_plane(points)
    print(plane)
    print(len(plane))
    #bpy.ops.mesh.select_all(action = 'DESELECT')
    for point in points:
        if point not in plane:
            point.select = False
    points = set(points) - plane
    print(len(points))
    print(points)
    bpy.ops.mesh.merge(type='CENTER', uvs=False)
    center = get_selected_vert(bm)
    center[0].select = False
    print('merget center',center)
    plane = find_plane(points)
    for point in points:
        if point in plane:
            point.select = True
    bpy.ops.mesh.merge(type='CENTER', uvs=False)
    center.append(get_selected_vert(bm)[0])
    print(center)
    edge1 = get_same_edge(center[0],center[1])
    bmesh.ops.delete(bm, geom = [edge1], context="EDGES")
    #bmesh.ops.pointmerge(bm, verts = list(plane), merge_co = Vector((list(plane)[0].co)))
    bmesh.update_edit_mesh(me)
    
remove_circle()

Update:
Now can remove multiple holes, but dosen’t always work. Gets to some kind of loop. I think it is because, it can separate same circles from same plane. Have to find different solution

import bpy
import bmesh

from mathutils import Vector
from itertools import combinations
import random
import time
import os
import numpy as np
os.system("cls")

def round_vector( vector, decimals ):
    # returns a tuple so that we avoid to allocate a new vector
    return (round(vector.x, decimals), round(vector.y, decimals), round(vector.z, decimals))


def find_plane( points, precision_decimals = 4 ):
    if type(points) is set:
        points = list(points)
    best_plane = set()
    planes = {} #One allocation for the set and the set is now a raw Python set
    for a in range( len(points) - 2 ):
        planes.clear() #Clear instead of allocating again
        pa = points[a]
        for b in range( a + 1, len(points) - 1 ): #Two loops instead of combinations
            pb = points[b] #Get once for all 'c'
            ab = pb.co - pa.co #Calculated once for all 'c'
            for c in range( b + 1, len(points) ):
                pc = points[c]
                ac = pc.co - pa.co
                normal = round_vector( ab.cross(ac).normalized(), precision_decimals ) #No freeze needed as we have a tuple
                plane = planes.get( normal )
                if not plane:
                    plane = {pa} #Immediately allocated raw set structure
                    planes[normal] = plane
                plane.add(pb)
                plane.add(pc)
                if len(best_plane) < len(plane): #Avoids if else assignation
                    best_plane = plane

    return best_plane
def get_selected_vert(bm):
    return [vertex for vertex in bm.verts if vertex.select]

def get_same_edge(vert1,vert2):
    for edge1 in vert1.link_edges:
        for edge2 in vert2.link_edges:
            if edge2 == edge1:
                return edge1

def separete_groups(verts):
    verts = list(verts)
    all = []
    vert_set = [verts[0]]
    #verts.pop(0)
    index = 0
    close = 1
    while verts:
        for edge in vert_set[-1].link_edges:
            for next_vert in edge.verts:
                print("point",vert_set[-1],next_vert, close)
                if next_vert in verts and close and next_vert != vert_set[-1]:
                    if (len(vert_set) > 1 and next_vert is not vert_set[-2]) or len(vert_set) == 1:
                        
                        #and next_vert not in vert_set[1:] and next_vert is not vert_set[-1]:
                        index += 1
                        print('next point',next_vert)
                        if next_vert == vert_set[0]:
                            print("poista")
                            all.append(vert_set)
                            verts = list(set(verts) - set(vert_set))
                            if verts:
                                vert_set = [verts[0]]
                                close = 0
                        else:
                            vert_set.append(next_vert)
                            print('list of all',vert_set, len(vert_set))
                            print(next_vert)
                            close = 0
                            #verts.remove(next_vert)
        close = 1
    return all
        
        
        
def remove_circle():
    me = bpy.context.edit_object.data
    bm = bmesh.from_edit_mesh(me)
    points = get_selected_vert(bm)
    print(len(points))
    while points:
        plane = find_plane(points)
        print(plane)
        print(len(plane))
        bpy.ops.mesh.select_all(action = 'DESELECT')
        #for point in points:
        #    if point not in plane:
        #        point.select = False
        points = set(points) - plane
        print(len(points))
        print(points)
        vert_group = separete_groups(plane)
        print('Different', vert_group, len(vert_group))
        center_points = []
        for circle in vert_group:
            for point in circle:
                point.select = True 
            bpy.ops.mesh.merge(type='CENTER', uvs=False)
            center_points.append(get_selected_vert(bm)[0])
            if center_points[-1] in points:
                points.remove(center_points[-1])
            center_points[-1].select = False
            #points.remove(center_points[-1])
        #center = get_selected_vert(bm)
        #center[0].select = False
        #print('merget center',center_points)
        #return 0
        #plane = find_plane(points)
        #for point in points:
        #    if point in plane:
        #        point.select = True
        #bpy.ops.mesh.merge(type='CENTER', uvs=False)
        #center.append(get_selected_vert(bm)[0])
        #print(center)
        #edge1 = get_same_edge(center[0],center[1])
        #bmesh.ops.delete(bm, geom = [edge1], context="EDGES")
        if len(points) < 5:
            break
    bmesh.ops.dissolve_verts(bm, verts = center_points)
        #bmesh.ops.pointmerge(bm, verts = list(plane), merge_co = Vector((list(plane)[0].co)))
    bpy.ops.mesh.select_all(action = 'DESELECT')
    bmesh.update_edit_mesh(me)
    
remove_circle()

1 Like

Now it gets the holes from the edges that have faces whose angle is 70 -110 degrees. So this works only if you have solid with hole through it. There is also other one, where it measures the length of the edge and tries to find loop with same length edges.

import bpy
import bmesh
import os
import numpy as np
from math import radians

os.system("cls")
def round_vector( vector, decimals ):
    # returns a tuple so that we avoid to allocate a new vector
    return (round(vector.x, decimals), round(vector.y, decimals), round(vector.z, decimals))


def find_plane( points, precision_decimals = 4 ):
    if type(points) is set:
        points = list(points)
    best_plane = set()
    planes = {} #One allocation for the set and the set is now a raw Python set
    for a in range( len(points) - 2 ):
        planes.clear() #Clear instead of allocating again
        pa = points[a]
        for b in range( a + 1, len(points) - 1 ): #Two loops instead of combinations
            pb = points[b] #Get once for all 'c'
            ab = pb.co - pa.co #Calculated once for all 'c'
            for c in range( b + 1, len(points) ):
                pc = points[c]
                ac = pc.co - pa.co
                normal = round_vector( ab.cross(ac).normalized(), precision_decimals ) #No freeze needed as we have a tuple
                plane = planes.get( normal )
                if not plane:
                    plane = {pa} #Immediately allocated raw set structure
                    planes[normal] = plane
                plane.add(pb)
                plane.add(pc)
                if len(best_plane) < len(plane): #Avoids if else assignation
                    best_plane = plane

    return best_plane


def get_selected_vert(bm):
    return [vertex for vertex in bm.verts if vertex.select]

def get_same_edge(vert1,vert2):
    for edge1 in vert1.link_edges:
        for edge2 in vert2.link_edges:
            if edge2 == edge1:
                return edge1


def separete_groups(verts):
    verts = list(verts)
    all_groups = []
    vert_set = [verts[0]]
    index = 0
    while index < len(verts):
        for edge in verts[index].link_edges:
            for next_vert in edge.verts:
                # print("point",vert_set[-1],next_vert, close)
                if next_vert in verts and next_vert != verts[index]:
                    result = find_loop_with_angle(verts, [next_vert], edge, verts[index], next_vert)
                    # result = find_loop_with_length(verts, [next_vert], edge, verts[index], next_vert, edge.calc_length())
                    # print("tulos:",verts[index], result,len(result))
                    if result:
                        verts = list(set(verts) - set(result))
                        all_groups.append(result)
                        if not verts:
                            return all_groups
        index += 1
    return all_groups

        
def find_center_circle(v1, v2, v3):
    A = np.array(co1.co)
    B = np.array(co2.co)
    C = np.array(co3.co)
    a = np.linalg.norm(C - B)
    b = np.linalg.norm(C - A)
    c = np.linalg.norm(B - A)
    s = (a + b + c) / 2
    R = a*b*c / 4 / np.sqrt(s * (s - a) * (s - b) * (s - c))
    b1 = a*a * (b*b + c*c - a*a)
    b2 = b*b * (a*a + c*c - b*b)
    b3 = c*c * (a*a + b*b - c*c)
    P = np.column_stack((A, B, C)).dot(np.hstack((b1, b2, b3)))
    P /= b1 + b2 + b3
    return 0

def find_loop_with_length(verts, loop_verts, next_edge, start_vert, next_input_vert, edge_length):
    for edge in next_input_vert.link_edges:
        if edge != next_edge and abs(edge_length - edge.calc_length()) < 1e-5:
            # print("edge", edge)
            for next_vert in edge.verts:
                if next_vert != next_input_vert and next_vert in verts and next_vert not in loop_verts:
                    loop_verts.append(next_vert)
                    if next_vert == start_vert: # loop found
                        # print("Out of loop",len(loop_verts), loop_verts)
                        return loop_verts
                    else:
                        find_loop_with_length(verts, loop_verts, edge, start_vert, next_vert, edge_length)
                        if loop_verts[-1] == start_vert:
                            return loop_verts
                    
    #print("no loop")
    return []

def find_loop_with_angle(verts, loop_verts, next_edge, start_vert, next_input_vert):
    # print("inside", len(start_vert.link_edges), next_edge)
    for edge in next_input_vert.link_edges:
        if edge != next_edge and (radians(70) < edge.calc_face_angle() < radians(110)):
            # print("edge", edge)
            for next_vert in edge.verts:
                if next_vert != next_input_vert and next_vert in verts and next_vert not in loop_verts: # is not last point, is in the select vertex list and is not already selected loop vertises
                    loop_verts.append(next_vert)
                    if next_vert == start_vert: # loop found
                        # print("Out of loop",len(loop_verts), loop_verts)
                        return loop_verts
                    else:
                        find_loop_with_angle(verts, loop_verts, edge, start_vert, next_vert)
                        if loop_verts[-1] == start_vert:
                            return loop_verts
                    
    #print("no loop")
    return []
                    
                        
        
def remove_circle():
    me = bpy.context.edit_object.data
    bm = bmesh.from_edit_mesh(me)
    points = get_selected_vert(bm)
    #print(len(points))
    center_points = []
    bpy.ops.mesh.select_all(action = 'DESELECT')
    all_groups = []
    while points:
        plane = find_plane(points) # All points that are in the same "virtual plane"
        #print(plane)
        #print(len(plane))
        points = set(points) - plane # Remove form selected list
        #print(len(points))
        #print(points)
        vert_group = separete_groups(plane)
        all_groups.append(vert_group)
        #print('Different', vert_group, len(vert_group))
        if len(points) < 5:
            break
    all_groups = [x for sublist in all_groups for x in sublist] # Make list flat
    #print(all_groups)
    for circle in all_groups:
        for point in circle:
            point.select = True
        
        bpy.ops.mesh.merge(type='CENTER', uvs=False)
        center_points.append(get_selected_vert(bm)[0])
        if center_points[-1] in points:
            points.remove(center_points[-1])
        center_points[-1].select = False
    print(center_points)
    bmesh.ops.dissolve_verts(bm, verts = center_points)
        #bmesh.ops.pointmerge(bm, verts = list(plane), merge_co = Vector((list(plane)[0].co)))
    bpy.ops.mesh.select_all(action = 'DESELECT')
    bmesh.update_edit_mesh(me)
    
remove_circle()

1 Like

Hmm…the tool is really slow. Any ideas how to make it faster?