1
$\begingroup$

Using Python I want to split a mesh into tiles. All the solutions to this question rely on bpy.ops.mesh.separate(type='LOOSE'). But this leads to problems in the following mesh:

36 objects instead of 10

I need 10 objects, named bisect-1, bisect-2 etc. But after bisecting with the code below the overhanging cubes on top become disconnected from the surface, and will end up in their own object as a result of bpy.ops.mesh.separate(type='LOOSE'). I end up with 36 objects instead of 10.

How to bisect a mesh into n parts, without separate 'LOOSE'?

import bpy, bmesh
from bpy import context as C
from mathutils import Vector

def bounds(obj, local=False):

    local_coords = obj.bound_box[:]
    om = obj.matrix_world

    if not local:    
        worldify = lambda p: om * Vector(p[:]) 
        coords = [worldify(p).to_tuple() for p in local_coords]
    else:
        coords = [p[:] for p in local_coords]

    rotated = zip(*coords[::-1])

    push_axis = []
    for (axis, _list) in zip('xyz', rotated):
        info = lambda: None
        info.max = max(_list)
        info.min = min(_list)
        info.distance = info.max - info.min
        push_axis.append(info)

    import collections

    originals = dict(zip(['x', 'y', 'z'], push_axis))

    o_details = collections.namedtuple('object_details', 'x y z')
    return o_details(**originals)

object_details = bounds(C.object, True)

bpy.ops.object.mode_set(mode='EDIT')

bm = bmesh.from_edit_mesh(C.object.data)

edges = []

# split the model into parts along y-axis, with steps of 10 in local coordinate space (this ignores the scale property, so make sure to apply it before)
for i in range(int(round(object_details.y.min)), int(round(object_details.y.max)), 10):
        ret = bmesh.ops.bisect_plane(bm, geom=bm.verts[:]+bm.edges[:]+bm.faces[:], plane_co=(0,i,0), plane_no=(0,1,0))
        bmesh.ops.split_edges(bm, edges=[e for e in ret['geom_cut'] if isinstance(e, bmesh.types.BMEdge)])


bmesh.update_edit_mesh(C.object.data)

bpy.ops.mesh.separate(type='LOOSE')
bpy.ops.object.mode_set(mode='OBJECT')

Edit: code changes after comment from batFINGER. Object isn't yet split after bisecting. But the 36 separate islands are identified. Now I need to group the islands based on the bisection coordinates, make separate objects and name them appropriately. But I'm not sure how to approach this.

import bpy, bmesh
from bpy import context as C
from mathutils import Vector

# https://blender.stackexchange.com/questions/75332/how-to-find-the-number-of-loose-parts-with-python/105142#105142
def walk_island(vert):
    ''' walk all un-tagged linked verts '''    
    vert.tag = True
    yield(vert)
    linked_verts = [e.other_vert(vert) for e in vert.link_edges
            if not e.other_vert(vert).tag]

    for v in linked_verts:
        if v.tag:
            continue
        yield from walk_island(v)

def get_islands(bm, verts=[]):
    def tag(verts, switch):
        for v in verts:
            v.tag = switch
    tag(bm.verts, True)
    tag(verts, False)
    ret = {"islands" : []}
    verts = set(verts)
    while verts:
        v = verts.pop()
        verts.add(v)
        island = set(walk_island(v))
        ret["islands"].append(list(island))
        tag(island, False) # remove tag = True
        verts -= island
    return ret

# https://blender.stackexchange.com/questions/32283/what-are-all-values-in-bound-box
def bounds(obj, local=False):

    local_coords = obj.bound_box[:]
    om = obj.matrix_world

    if not local:    
        worldify = lambda p: om * Vector(p[:]) 
        coords = [worldify(p).to_tuple() for p in local_coords]
    else:
        coords = [p[:] for p in local_coords]

    rotated = zip(*coords[::-1])

    push_axis = []
    for (axis, _list) in zip('xyz', rotated):
        info = lambda: None
        info.max = max(_list)
        info.min = min(_list)
        info.distance = info.max - info.min
        push_axis.append(info)

    import collections

    originals = dict(zip(['x', 'y', 'z'], push_axis))

    o_details = collections.namedtuple('object_details', 'x y z')
    return o_details(**originals)

object_details = bounds(C.object, True)

bpy.ops.object.mode_set(mode='EDIT')

bm = bmesh.from_edit_mesh(C.object.data)

edges = []

# split the model into parts along y-axis, with steps of 10 in local coordinate space (this ignores the scale property, so make sure to apply it before)
for i in range(int(round(object_details.y.min)), int(round(object_details.y.max)), 10):
        ret = bmesh.ops.bisect_plane(bm, geom=bm.verts[:]+bm.edges[:]+bm.faces[:], plane_co=(0,i,0), plane_no=(0,1,0))
        bmesh.ops.split_edges(bm, edges=[e for e in ret['geom_cut'] if isinstance(e, bmesh.types.BMEdge)])


bmesh.update_edit_mesh(C.object.data)

# bpy.ops.mesh.separate(type='LOOSE')

islands = [island for island in get_islands(bm, verts=bm.verts)["islands"]]
print("Islands:", len(islands))
print([len(i) for i in islands])

bpy.ops.object.mode_set(mode='OBJECT')
$\endgroup$
  • $\begingroup$ Might find this answer useful. Instead of separating into new objects returns a dictionary of the islands. $\endgroup$ – batFINGER Sep 21 '18 at 13:02
  • $\begingroup$ Thought there was something familiar about this. After running your bisects, get the list of islands using code from comment above. Each island will belong to a bisect based on coords. There may be none, one or more islands per bisect. From there could split by selection, or (possibly simpler-ish) join the split objects that share verts in a bisect. $\endgroup$ – batFINGER Sep 21 '18 at 13:32
  • $\begingroup$ This is interesting! Code from first comment finds 36 islands. So how do I group these into 10 objects? I'd like to name them bisect-1, bisect-2 etc. Do you think I need this answer to make separate objects? $\endgroup$ – Jip Sep 21 '18 at 14:01
  • $\begingroup$ Yes that would be one way. Or use something like left - right test to see if (say centroid) point of each split is between planes. Also there is a clear inner (or outer) on the bisect plane operator. Might be easier to step thru this way with each set of two planes. ie copy bmesh bisect clear inner on plane i, and outer on plane i+1 write to new mesh... for each section. example of clear outer $\endgroup$ – batFINGER Sep 21 '18 at 14:25
2
$\begingroup$

Using clear inner and outer on bisect plane operator.

Further to my comment, think this may be the simplest way.
Test script, which I've tested on Suzanne (who has seperate eye objects) and appears to work correctly.

Test script uses bmesh of original object, then for each set of two planes bisects and clears inner on left hand plane, and outer on right hand plane on a copy of the original bmesh.

Run in object mode with object to split selected. Creates a new object for each split. Doesn't remove original.

(for example sake splits in along local z axis in units from -10 to 10)

import bpy, bmesh
from bpy import context as C
from  mathutils import Vector

def newobj(bm):
    me = bpy.data.meshes.new("split")
    bm.to_mesh(me)
    ob = bpy.data.objects.new("split", me)
    C.scene.objects.link(ob)

bmo = bmesh.new()
bmo.from_mesh(C.object.data)

planes = [Vector((0, 0, i)) for i in range(-10, 10, 1)]
p0 = planes.pop(0)
while planes:
    bm = bmo.copy()
    p1 = planes.pop(0)    


    bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
            plane_co=p0, 
            plane_no=(0,0,1), 
            clear_inner=True)

    bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
            plane_co=p1,
            plane_no=(0,0,1), 
            clear_outer=True)
    if len(bm.verts):
        newobj(bm)
    p0 = p1

Naming the bisects based on each plane pair should be trivial after determining the correct splits as you have in question.

$\endgroup$
3
$\begingroup$

Thanks to the comments of batFINGER I came up with a nearly identical solution. Main difference is that I cut again from the remains of the previous iteration, gradually reducing the mesh to bisect. I guess this performs better than bisecting from the original bmesh each iteration.

import bpy, bmesh
from bpy import context as C
from mathutils import Vector

def newobj(bm, name):
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    ob = bpy.data.objects.new(name,me)
    C.scene.objects.link(ob)
    return ob

# https://blender.stackexchange.com/questions/32283/what-are-all-values-in-bound-box
def bounds(obj, local=False):

    local_coords = obj.bound_box[:]
    om = obj.matrix_world

    if not local:    
        worldify = lambda p: om * Vector(p[:]) 
        coords = [worldify(p).to_tuple() for p in local_coords]
    else:
        coords = [p[:] for p in local_coords]

    rotated = zip(*coords[::-1])

    push_axis = []
    for (axis, _list) in zip('xyz', rotated):
        info = lambda: None
        info.max = max(_list)
        info.min = min(_list)
        info.distance = info.max - info.min
        push_axis.append(info)

    import collections

    originals = dict(zip(['x', 'y', 'z'], push_axis))

    o_details = collections.namedtuple('object_details', 'x y z')
    return o_details(**originals)

object_details = bounds(C.object, True)

bpy.ops.object.mode_set(mode='OBJECT')

cut_index = 0
step_size = 10

bisection_outer = bmesh.new()
bisection_outer.from_mesh(C.object.data)

# split the model into parts along y-axis, with steps of 10 in local coordinate space (this ignores the scale property, so make sure to apply it before)
for i in range(int(round(object_details.y.min)), int(round(object_details.y.max+step_size)), step_size):
        bisection_inner = bisection_outer.copy()
        bmesh.ops.bisect_plane(bisection_outer, geom=bisection_outer.verts[:]+bisection_outer.edges[:]+bisection_outer.faces[:], plane_co=(0,i,0), plane_no=(0,1,0), clear_inner=True)
        bmesh.ops.bisect_plane(bisection_inner, geom=bisection_inner.verts[:]+bisection_inner.edges[:]+bisection_inner.faces[:], plane_co=(0,i,0), plane_no=(0,1,0), clear_outer=True)

        newobj(bisection_inner, "bisect-"+str(cut_index))
        bisection_inner.free()  # free and prevent further access
        cut_index+=1

bisection_outer.free()  # free and prevent further access

bpy.data.objects.remove(C.object, True)
$\endgroup$
  • $\begingroup$ Nice one, agree using cut down mesh each time more efficient, didn't bother for example sake. Prob. no need for edit mode also. Please consider upvoting too if you find questions and answers useful. $\endgroup$ – batFINGER Sep 21 '18 at 15:47
  • $\begingroup$ Thanks you're right. Works in object mode like your solution. Upvoted :) $\endgroup$ – Jip Sep 21 '18 at 15:50
  • $\begingroup$ It removes materials.. $\endgroup$ – Jan Kadeřábek Jan 18 at 18:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.