# How do I make this chunk separation algorithm I've been working on?

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-start) / cell_size))
y_segments = (math.ceil((end-start) / cell_size))
z_segments = (math.ceil((end-start) / 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),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,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),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
y = args
x = args
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 // cell_size
y = co // cell_size
z = co // 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:
object.name = name
elif delete == 1:
bpy.context.view_layer.objects.active = obj
obj.select_set(True)


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?

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

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.
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:


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. • 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. Jul 5, 2022 at 9:56
• 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. Jul 9, 2022 at 10:58
• 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'` Jul 9, 2022 at 11:03
• Dude you've so helpful thank you so much! Jul 9, 2022 at 18:37
• :D I'll make sure to give you credit Jul 10, 2022 at 14:53