2
$\begingroup$

I need to automate the process of joining similar objects from an imported .IFC file from Revit, this can take quite long time to sort out manually because this files can contain from 600 to 800 objects. The names have "." and ":" that I can use for separate the strings and join the same name objects.

I´m not a programmer, and I don't have experience with Python, but after some studying I manage to write this little script.

script for joining same name objects

But I'm kinda lost here because it works fine with small list of objects but it crashes when dealing with longer lists.

In this one it runs ok, at the end all cubes are one object, spheres are other object, etc.

Script running ok

But when I run it against more longer lists of objects, it crashes Blender:

Script run Fails

I'm assuming it has something to do when it tries to read objects that are no longer in the list.

Do you know if this assumption is correct? If so could you suggest a way to avoid the crash? Is there a better way to do what I'm trying to do based on the names of the objects?

This is script:

import bpy
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.objects.active = None

for obs in bpy.context.scene.objects:
    if obs.name in bpy.context.view_layer.objects:     
        if obs.type == 'MESH':
            bpy.ops.object.select_all(action='DESELECT')
            nocolon = obs.name.rsplit(maxsplit=1, sep=':')
            print ('nocolon pass done for ' + nocolon[0])
            tname = nocolon[0].rsplit(maxsplit=1, sep='.')
            print ('nodot pass done for ' + tname[0])
            bpy.ops.object.select_pattern(pattern= tname[0]+'*', case_sensitive=False, extend=True)
            print ('selection pass done for ' + tname[0])
            obs.select_set(True)
            print ('selection set done for ' + tname[0])
            bpy.context.view_layer.objects.active = obs
            print ('set active object done for ' + tname[0])
            bpy.ops.object.join()
            print ('join pass done for ' + tname[0])
            print ('iteration done for ' + tname[0])
            bpy.context.view_layer.objects.active = None

        else:
            print(obs.name + ' is not a MESH.')

    else:
        print('There is no ' + obs.name + ' in view_layer')
        bpy.data.objects.remove(obs)
        print('removed from data')

print('script done')

I'm using Blender 2.81.16a

$\endgroup$
5
  • $\begingroup$ You could collect all objects that you want to join in a list and select and join them outside of the loop. $\endgroup$ Commented Feb 9, 2020 at 22:50
  • $\begingroup$ This is an excellent question and you're absolutely right that the crash occurs when accessing the next object when iterating through bpy.context.scene.objects. I'm not entirely sure if this is supposed work, since the join essentially modifies the collection that is iterated. I'll post a bug report. $\endgroup$ Commented Feb 9, 2020 at 23:01
  • $\begingroup$ The bug report can be found here. $\endgroup$ Commented Feb 9, 2020 at 23:01
  • $\begingroup$ Thanks for the corrections in the spelling Duarte. English is not my native tongue, but i'll try to be more careful when writing. $\endgroup$ Commented Feb 10, 2020 at 3:30
  • $\begingroup$ Thanks Robert for the insight. that sounds more logical (Dont touch the object lists) I still need to figure out how to use a custom list to join the objects, but in the meantime I'm trying to use the collections instead. Do you know how to get the collection_index from a collection once is created? I can't find how to get it. I need it to be able to move objects to a collection with the bpy.ops.outliner.item_activate(extend=False, deselect_all=True) bpy.ops.object.move_to_collection. It seems I need the collection number, but I cant find it anywhere. $\endgroup$ Commented Feb 10, 2020 at 3:38

2 Answers 2

3
$\begingroup$

Using Operator override

Here are a couple of examples. This one finds all the mesh objects in the scene, sorts them alphabetically by name, then groups them by if they start with the first.

meshobs ['Cone', 'Cone.001', 'Cone.002', 'Cube', 'Cube.001', 'Cube.002', 'Cylinder', 'Cylinder.001', 'Cylinder.002']
Join ['Cone', 'Cone.001', 'Cone.002']
Join ['Cube', 'Cube.001', 'Cube.002']
Join ['Cylinder', 'Cylinder.001', 'Cylinder.002']

This is only a proof of concept and relies on "Cube" being in scene to match "Cube.nnn", add string partitioning etc as required.

The names of the matching objects are sent to the join method, which calls the operator with a context override dictionary, for active object and selected objects.

Note this Uses only the join operator which will greatly reduce the amount of operator calls from question code

Python performance with Blender operators

import bpy

context = bpy.context
scene = context.scene

def join(names):
    obs = list(filter(None, (scene.objects.get(n) for n in names)))
    print(obs)

    obs[0].select_set(True)
    bpy.ops.object.join(
            {"active_object" : obs[0],
             "object" : obs[0],
             "selected_objects" : obs,
             "selected_editable_objects" : obs
             })


meshobs = [o.name for o in scene.objects if o.type == 'MESH']
meshobs.sort()

while meshobs:
    n0 = meshobs[0]
    x = [n for n in meshobs if n.startswith(n0)]
    print("Joining", x)
    join(x)
    meshobs = meshobs[len(x):]

Alternatively using bmesh and no operators

Can use bmesh to read in first mesh, then convert each other mesh into object space of first, and write back to first, then remove others.

This will keep only the materials of the first, etc etc, and may have to be altered accordingly.

import bpy
import bmesh


context = bpy.context
scene = context.scene

def bmesh_join(names):
    obs = list(filter(None, (scene.objects.get(n) for n in names)))
    print(obs)
    bm = bmesh.new()
    ob = obs.pop(0)
    mwi = ob.matrix_world.inverted()
    me = ob.data
    bm.from_mesh(me)
    for o in obs:
        o.data.transform(mwi @ o.matrix_world)
        bm.from_mesh(o.data)
        bpy.data.objects.remove(o)
    bm.to_mesh(me) 
    bm.free()
    me.update()   

meshobs = [o.name for o in scene.objects if o.type == 'MESH']
meshobs.sort()

while meshobs:
    n0 = meshobs[0] # fix for lpart on . : etc
    x = [n for n in meshobs if n.startswith(n0)]
    print("Join", x)
    bmesh_join(x)
    meshobs = meshobs[len(x):]
$\endgroup$
3
  • $\begingroup$ Thanks a lot batFINGER, those are good examples to study. I'll try to implement them and let you know how it goes.Thank you very much. $\endgroup$ Commented Feb 10, 2020 at 15:15
  • $\begingroup$ I googled this post while having the same-ish issue. Where do these options for bpy.ops.object.join() come from? I don't see them listed in the API docs... $\endgroup$ Commented Mar 9, 2021 at 8:48
  • $\begingroup$ Consult the docs. All operators can be passed an execution context eg 'INVOKE_DEFAULT' as first argument, an override dictionary as second, and then any / all of their keyword arguments. Recent example here blender.stackexchange.com/posts/comments/361824?noredirect=1 $\endgroup$
    – batFINGER
    Commented Mar 9, 2021 at 9:11
-1
$\begingroup$

Thanks everyone for you time. I manage to write a usable script with your insights and suggestions.

Robert: I was able to save the lists of names to variables. But I did not find a way to use those lists for joining the objects. I guess I need more study.

I ended up using the collections system as temporal holders for the objects. That way blender wont crash with big scenes. I still need to find a way to move objects to the collections that already exists. But I need to get the "collection_index" number from the target collection and I could not find a way to get it. In the meantime the script will create a lot of empty collections and at the end of the loop will remove them.

batFINGER: I'll try to polish / remake the script using the methods that you mention. I'm sure they are more elegant and less messier that the code that I'm using. They will help me to improve a lot in using Python.

This is the script, hopefully it can be of some utility for someone else:

#### This script can help you when importing models from architectural programs like Revit.
#### It joins objects with similar names.
#### WARNING: This should be used on a clean scene after importing the model from revit (I recommend using .ifc because FBX generates a more tessellated meshes) (addon for blender 2.8x in https://blenderbim.org/)
#### WARNING: It will remove collections from your scene and leave all objects on the master collection.
#### WARNING: It will remove all materials from the scene.
#### WARNING: After importing .ifc or your model, make sure that all Collections are Visible in the outliner. Otherwise it can be ignored and generate some issues when joining objects.

import time
then = time.time() #Time before operations start
import bpy

# Deselect everything
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.objects.active = None

#Move all objects to master Collection and remove empty collections.
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.move_to_collection(collection_index=0)
bpy.ops.object.select_all(action='DESELECT')

for cv in bpy.data.collections:
    cv.hide_viewport=False

for c in bpy.context.scene.collection.children:
    bpy.context.scene.collection.children.unlink(c)
    bpy.data.collections.remove(c)

#Clear parents and removing empties from scene

for np in bpy.context.scene.objects:
    print('Clearing parents for ' + np.name)
    np.select_set(True)
    bpy.context.view_layer.objects.active = np
    bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
    bpy.ops.object.select_all(action='DESELECT')
    print('Parent cleared for ' + np.name)

for empt in bpy.context.scene.objects:
    if empt.type == 'EMPTY' and len(empt.children) == 0:
        bpy.data.objects.remove(empt)
        print('Removing Empty') 
    else:
        continue

#Selecting the objects with the same name and moving them to temporal collections.

for obs in bpy.data.objects:
        if obs.name in bpy.context.view_layer.objects:
            if obs.type == 'MESH':
                bpy.ops.object.select_all(action='DESELECT')
#                nocolon = obs.name.rsplit(maxsplit=1, sep=':')
                nocolon = obs.name.rsplit(sep=':')
                print ('nocolon pass done for ' + nocolon[0])
#                tname = nocolon[0].rsplit(maxsplit=1, sep='.')
                tname = nocolon[0].rsplit(sep='.')
                print ('nodot pass done for ' + tname[0])
                bpy.ops.object.select_pattern(pattern= tname[0]+'*', case_sensitive=False, extend=True)
                print ('selection pass done for ' + tname[0])
                obs.select_set(True)
                print ('selection set done for ' + tname[0])
                bpy.context.view_layer.objects.active = obs
                print ('set active object done for ' + tname[0])
                bpy.ops.object.move_to_collection(collection_index=0, is_new=True, new_collection_name=tname[0])        
                print ('Moving Objects to a new collection called ' + tname[0]) 
                print ('iteration done for ' + tname[0])
                bpy.context.view_layer.objects.active = None
            else:
                print(obs.name + ' is not a MESH.')
        else:
            print('There is no ' + obs.name + ' in view layer')

# Clearing the empty collections
for col in bpy.data.collections:
        if len(col.objects) > 0:
            print("The Collection " + col.name + "has the objects")
        else:
            bpy.data.collections.remove(col)

#Joining objects inside each collection
bpy.ops.object.select_all(action='DESELECT')

for col in bpy.data.collections:
        print('Im about to join the objects in ' + col.name)
        bpy.context.view_layer.objects.active = col.objects[0]
        bpy.ops.object.select_grouped(type='COLLECTION')
        bpy.ops.object.join()
        bpy.ops.object.select_all(action='DESELECT')
        bpy.context.view_layer.objects.active = None
        print('Done joining the objects in ' + col.name)

#Move all objects to the master collection and clearing all collections created.
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.move_to_collection(collection_index=0)
bpy.ops.object.select_all(action='DESELECT')

for c in bpy.context.scene.collection.children:
    bpy.context.scene.collection.children.unlink(c)        

# Removing dummy objects from IFC file.
for OE in bpy.context.scene.objects:
    if OE.name.startswith("IfcOpeningElement"):
        print('Removing Ifc Dummy object: ' + OE.name)
        bpy.data.objects.remove(OE)
    else:
        continue

#Clears all materials from the scene:
print('Clearing all materials from scene')
for material in bpy.data.materials:
    material.user_clear()
    bpy.data.materials.remove(material)
    print('Done clearing materials')

for obs in bpy.context.scene.objects:
    if obs.type == 'MESH':
        obs.select_set(True)
        bpy.context.view_layer.objects.active = obs
        bpy.ops.object.editmode_toggle()
        bpy.ops.mesh.tris_convert_to_quads()
#        bpy.ops.mesh.remove_doubles()
#        bpy.ops.mesh.normals_make_consistent()
        bpy.ops.object.editmode_toggle()
        bpy.ops.object.select_all(action='DESELECT')
        bpy.context.view_layer.objects.active = None
    else:
        continue


print('script done')
now = time.time() #Time after all aperations done
print("It took: ", now-then, " seconds")
$\endgroup$

You must log in to answer this question.

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