2
$\begingroup$

EDIT: Here is the .blend file:

EDIT 2: Here is the mesh I want it to work on if you're interested.


It's being used for a game I'm making but there is a maximum mesh size which is 20.48 Blender Units (The cube is just the region showing the mesh size limit)

I've been working on an algorithm that takes a mesh which has its origin to its geometry and bisects it into cubic chunks of a specified size, selects the faces of these chunks and then separates them. After each separation, it cleans up the scene by removing meshes with dimensions of more than 2 axes of 0, and adding a solidify modifier to meshes where only one axis has a dimension of zero.

However, when assembling each of these 5 steps an error occurs and I don't understand how to debug these errors.

I have imported bpy, time and math.

Here is the code for step 1 where the mesh is bisected into cubic chunks:

select function:

def select(obj):
    if bpy.ops.object.mode_set.poll():
        bpy.ops.object.mode_set(mode='OBJECT')
    else:
        return
    verts = obj.data.vertices
    edges = obj.data.edges
    faces = obj.data.polygons
    for v in verts:
        v.select = True
    for e in edges:
        e.select = True
    for f in faces:
        f.select = True
    if bpy.ops.object.mode_set.poll():
        bpy.ops.object.mode_set(mode='EDIT')

Bisection:

bpy.context.view_layer.objects.active = obj
so = obj
#so means 'selected object'
name = so.name

start = [0,0,0]

dim = so.dimensions
loc = so.location

cell_size = size 

for i in range(3):
    start[i] = math.floor((loc[i] - (dim[i]/2))/cell_size)*cell_size
  
    
if output:
    print(start, end)

x_segments = (math.ceil((end[0]-start[0]) / cell_size))
y_segments = (math.ceil((end[1]-start[1]) / cell_size))
z_segments = (math.ceil((end[2]-start[2]) / cell_size))

if output:    
    print(x_segments,y_segments,z_segments)
    
if bpy.ops.object.mode_set.poll():
    bpy.ops.object.mode_set(mode='EDIT')
for x in range(x_segments+1):
    select(so)
    if bpy.ops.mesh.bisect.poll():
        bpy.ops.mesh.bisect(plane_co=((x*cell_size) + start[0],0,0),plane_no=(1,0,0))
for y in range(y_segments+1):
    select(so)
    if bpy.ops.mesh.bisect.poll():
        bpy.ops.mesh.bisect(0,plane_co=((y*cell_size) + start[1],0),plane_no=(0,1,0))
for z in range(z_segments+1):
    select(so)
    if bpy.ops.mesh.bisect.poll():
        bpy.ops.mesh.bisect(0,0,plane_co=((z*cell_size) + start[2]),plane_no=(0,0,1))
if output:
    print("Bisected")

For the separation I used the centre of the polygons which, due the bisection, should never be overlapping two chunks. I also utilised a dictionary of lists containing the faces to separate into a mesh.

Vector Hash function:

def VectorHash(*args):
    x,y,z = 0,0,0

    x = args[0]
    y = args[1]
    x = args[2]
    return x * 314159 + y * 232357 + z * 998873

Separation: if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='OBJECT')

global cells
cells = {}

vertices = so.data.vertices

polygons = so.data.polygons

for face in polygons:
    #Get global face center coordinates
    local_co = face.center
    co = so.matrix_world @ local_co

    x = co[0] // cell_size
    y = co[1] // cell_size
    z = co[2] // cell_size
    print(VectorHash(x, y, z))
    try:
        cells[str(VectorHash(x, y, z))].append(face)
    except:
        cells[str(VectorHash(x, y, z))] = list((face))

print("Cells are: ",cells)

for cell in cells.copy().values():
    
    if bpy.ops.object.mode_set.poll():
        bpy.ops.object.mode_set(mode='OBJECT')
    
    for face in polygons:
        face.select = False
    
    for face in cell:
        face.select = True
        
    if bpy.ops.object.mode_set.poll():
        bpy.ops.object.mode_set(mode='EDIT')
    try:
        selected = False
        for face in polygons:
            if face.select:
                selected = True
        if selected:
            if bpy.ops.mesh.separate.poll():
                bpy.ops.mesh.separate(type='SELECTED')
    except:
        pass
    if output:
        print("Done")
    #Get the largest object to apply the separation on again
    for obj in bpy.context.view_layer.objects:
        bpy.context.view_layer.objects.active = obj
        obj.select_set(False)
        if name in obj.name:
            chunk = True
            for dimension in obj.dimensions:
                if dimension > cell_size:
                    chunk = False
            if not chunk:
                so = obj
            bpy.context.view_layer.objects.active = so
        
    cleanup()

And here is the cleanup function that deletes useless meshes

def cleanup():
    for obj in bpy.data.objects:
        if obj.name == "Camera" or obj.name == "Light":
            continue
        print(obj.name,obj.dimensions)
        delete = 0
        for d in obj.dimensions:
            if d <= 0.05:
                delete = delete + 1
        verts = 0
        edges = 0
        faces = 0
        
    for v in obj.data.vertices:
        verts += 1
    for e in obj.data.edges:
        edges += 1
    for f in obj.data.polygons:
        faces += 1
    
    if faces <= 0:
        delete = True
    if delete > 1 or delete == True:
        #Get the name so as to rename the objects
        name = obj.name
        bpy.data.objects.remove(obj)
        
        for object in bpy.data.objects:
            if name in object.name:
                #Blender to readjust the name
                object.name = name
    elif delete == 1:
        bpy.context.view_layer.objects.active = obj
        obj.select_set(True)
        if bpy.ops.object.modifier_add.poll():
            bpy.ops.object.modifier_add(type='SOLIDIFY')

The problem is that sometimes things such as poll functions don't return true so important functions aren't executed, or collections like so.data.polygons is empty because it selected an empty mesh or didn't select one at all and I don't know what makes it work and what doesn't.

How do I assemble these in a full proof way that works without confusing to debug problems?

$\endgroup$
2
  • $\begingroup$ which blender version are you using? $\endgroup$ Jul 2, 2022 at 0:45
  • $\begingroup$ I am using Blender 2.92 $\endgroup$ Jul 2, 2022 at 7:41

1 Answer 1

2
$\begingroup$

I can't tell all of what is going on with your code, there seems to be missing sections.

Also you didn't post the error code so I can't really tell exactly how to fix your one specific problem.

Your question however seems more about philosophical "How to Write better Python" type question, so maybe I can offer a few pointers.

I think overall you should stop polling every 5 lines in your script. Theres a rule of thumb in Python known as Easier to Ask Forgiveness than Permission which in your context just means: stop asking Blender if you CAN do a thing before doing it, just do it and if an error occurs, either write an exception or correct the code to handle that case.

Like here:

def select(obj):
    # just call this method by itself, you don't need to poll.
    # if you're in edit mode, it does what you want, if not it does nothing.
    # ask forgiveness, not permission.
    bpy.ops.object.mode_set(mode='OBJECT')
    verts = obj.data.vertices
    edges = obj.data.edges
    faces = obj.data.polygons
    for v in verts:
        v.select = True
    for e in edges:
        e.select = True
    for f in faces:
        f.select = True
    # then, again just call the method you want.
    # if it does what you expect, great.
    # if not, it should do nothing.
    # you can write a try:, except block to handle it.
    # stop using plain except clauses.
    # always find the exact error, like AttributeError, or IndexError
    # and cite that in the exception.
    # a plain except will also bypass on NameError if you forgot to import something.
    bpy.ops.object.mode_set(mode='EDIT')

Generally speaking your code is also just way too full of if statements and between that and using poll() every 5 seconds it's no wonder half your code isn't running. Try to find ways to simplify the code, find a way to get one "final answer" as to whether you should go through this or that execution statement.

Like in this function:

# extract certain functionality to a separate function that is easier to 
# debug if there are issues, and easier to reuse. Clean code is easier to 
# troubleshoot.

def invalid_dimensions(dims, threshold=.05):

    # Read the book `Python Cookbook by David Beazley` 
    # you can find out alot of ways like this to use Python to 
    # it's strengths and get the same data quicker and easier.
    # returns the number of valid dimensions for an object

    return sum([v <= threshold for v in dims])

def cleanup():

# use a list comprehension to get all the objects you want in one place, 
# THEN loop through
# rather than checking every time if it's the correct object
# with some `if object.type == "CAMERA" or object.type == "EMPTY":
# branching statement.

    objs = [obj for obj in bpy.data.objects if obj.type == "MESH"]
    for obj in objs:
        bpy.context.view_layer.objects.active = obj
        print(obj.name,obj.dimensions)
        # stop doing all the weird addition stuff and find the minimum amount
        # of info to accomplish what you need. 
        if invalid_dimensions(obj.dimensions) > 1 or len(obj.data.polygons) < 1:
            obj.select_set(True)
            bpy.data.objects.remove(obj)
        else:
            bpy.ops.object.modifier_add(type='SOLIDIFY')

That takes something that was 35 lines of code down to 14, including whitespace between functions. Loads easier to read and debug.

EDIT:

I wrote an addon to do this. It can be found on my GitHub page. You can use it entirely if you want, or just look at it to compare.

enter image description here

$\endgroup$
22
  • 1
    $\begingroup$ I'm not sure what to tell you, that probably doesn't have to do with my code, that might be a bug. If you have specific problems with your script that you want solved then you should post the error Traceback and specific problems. Right now all you've done is posted almost your entire script and just asked "How do i make it work?". We can only do so much with that. $\endgroup$
    – Jakemoyo
    Jul 5, 2022 at 9:56
  • 1
    $\begingroup$ Oh my God, I just really wanted to thank you. I'm so new to blender python scripting and python scripting in general and I'm so so grateful that you've been so patient over the course of a week to answer my questions and look at bugs in my code that may seem simple to you. I'm just so glad that someone like you took time out of your day to rewrite my terrible code to make an addon and I'm just so happy that you've helped me and taught me. I will most certainly use your logic and philosophy in all my future programming. So again I say, thank you. $\endgroup$ Jul 9, 2022 at 10:58
  • 1
    $\begingroup$ I have installed the zip of the My Pie Menus add on but it's getting an error from your init.py file saying that Traceback (most recent call last): File "C:\Program Files\Blender Foundation\Blender 3.2\3.2\scripts\modules\addon_utils.py", line 335, in enable mod = __import__(module_name) File "C:\Users\Godwin\AppData\Roaming\Blender Foundation\Blender\3.2\scripts\addons\my_pie_menus-master\__init__.py", line 33, in <module> from my_pie_menus.resources import utils ModuleNotFoundError: No module named 'my_pie_menus' $\endgroup$ Jul 9, 2022 at 11:03
  • 1
    $\begingroup$ Dude you've so helpful thank you so much! $\endgroup$ Jul 9, 2022 at 18:37
  • 1
    $\begingroup$ :D I'll make sure to give you credit $\endgroup$ Jul 10, 2022 at 14:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .