Stuck making a simple script

I’m not very great at scripting, and this is my first blender attempt. But I tried pretty hard before asking here. So hopefully you all will go a Little easy on me.

I’m trying to get Rhino 3d data into blender. Someone made an awesome script for that. Yay, it puts rhino layers as collections. Imports curves (incorrectly), sort of import materials? It’s not perfect. But it’s a great start. BUT, Rhino Mesh data has a Lot of double vertices. Every NURBS trim object is linked and joined as a single object, but that means the edges of each have overlapping vertices (hopefully. and most of the time yes).

So I made this script that is Supposed to: shade smooth, use auto smooth, set angle to 45 merge by distance, .001.
I put a couple of those items up top as a variable so they were easier to change.

I got it to work on a basic sample scene of a few balls that I seperated and made flat shaded.

Some issues I had along the way were, it would set all the objects to smooth, but would only change object data settings to the active object. I was using bpy.context.scene.selected_objects
I found someone else’s script that said scene.objects and they were doing essentially what I was tryinig to do, only it was a 2.79 script and it failed and I couldn’t figure out why.

So I tried running this on the imported rhino file, and it’s just hung up. I’m guessing crashed? Not sure how long it takes to process 665K verts. 60 objects.

I’m guessing I need to check if the object is mesh? Because it’s importing the curves and those are at least Named object. Anyway, I thought I’d post it and see if one of you friendly people who know what they are doing could let me know where I went wrong.

import bpy

SmoothAngle = 45
selection = bpy.context.selected_objects
MergeThreshold = .0001

if bpy.context.mode != 'OBJECT':
    bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')


for things in bpy.context.scene.objects  : #bpy.context.selected_objects:
    bpy.ops.object.shade_smooth()
    bpy.context.object.data.use_auto_smooth = True
    bpy.context.object.data.auto_smooth_angle = 0.0174533 * SmoothAngle
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.remove_doubles(threshold= MergeThreshold )
    bpy.ops.object.mode_set(mode='OBJECT')
    
bpy.ops.object.select_all(action='DESELECT')

you’re close. you’re running the object.shade_smooth op on every object in the scene, but then trying to set autosmooth at the data-level, which won’t work since you’re doing it to a single object instead of looping over every object in the selection. either way, there is more than one way to do this, I would take a slightly different approach that doesn’t involve messing with the user’s selection or anything like that. Also, you are correct- you will need to check if an object is a mesh or you’ll get an attribute exception when you try to set autosmooth on a non-mesh data type.

import bpy, bmesh
from math import radians

SmoothAngle = 45
selection = bpy.context.selected_objects
MergeThreshold = .0001

ctx = bpy.context.copy()
# your method of calculating the radians is fine, but this is far more readable. 
# also, calculate it before the loop and store it as smooth_radians so it's not done on every loop
smooth_radians = radians(SmoothAngle) 

for o in bpy.context.scene.objects:
    if not isinstance(o.data, bpy.types.Mesh):
        continue

    # create a context override so you don't need to handle selecting/deselecting objects to make the operators work correctly
    ctx['object'] = o
    ctx['active_object'] = o
    ctx['selected_objects'] = [o]
    ctx['selected_editable_objects'] = [o]
    bpy.ops.object.shade_smooth(ctx)

    o.data.use_auto_smooth = True   
    o.data.auto_smooth_angle = smooth_radians

    bm = bmesh.new()
    bm.from_mesh(o.data)
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=MergeThreshold)
    bm.to_mesh(o.data)
    bm.free()

So, yeah, that works great. And thank you Very much. But I have some questions, if you are willing to help me learn a bit. I looked up as much as I could. You’ve done some things I’ve not seen yet. But like I said, all I’ve found so far, for beginners, is creating objects with python code. And I found one awesome video by Danny Mac 3D. It was Quite helpful…and that’s the kind of thing I am interested in learning. But it didn’t get me far enough to do this obviously.

I commented in the code that hopefully you, or someone, can help with. I can read the Python API, but quite a bit of it is assuming that I know how to program already. So doesn’t always help me understand something.

import bpy, bmesh  #Someone mentioned that the import commands aren't needed any longer? 
from math import radians #Yeah, I calculated the radian number ahead of time rather than using the math command. Since the angle threshold isn't actually All that important to be accurate I thought it might actually run faster if I just gave it a number. But you're right, this is more understandable. When I go back to it, it won't just be some random looking number later on. 

SmoothAngle = 45
selection = bpy.context.selected_objects
MergeThreshold = .0001

ctx = bpy.context.copy() #ctx variable creation, ok, you're avoiding writing bpy.context over and over. I like that, but I don't understand the copy() part.

smooth_radians = radians(SmoothAngle) 

for o in bpy.context.scene.objects:
    if not isinstance(o.data, bpy.types.Mesh):  #It Looks to me like you made a function named isinstance, but I don't understand how that works. isinstance doesn't exist already right, so you've named it that? I found it being used once in the Python API documentation, but only in an example code without any explaination. It seems like it's looking at the current object's data, and also checking if it is a mesh.  I'm not understanding this one, and can't find results with google that help me understand it. ifinstance( but how do I know what goes here?)
        continue 

    # There's not much about this block I understand. This creates a context override to handle the different types of objects, like if it's selected, editable, active, etc. But I don't understand how the brackets work, and why the = o, then on the other two = [o]. I don't fully understand what brackets are doing in python yet though either. Best I can find is that it brackets hold lists. But I'm confused if you are saying, an object is o, then an active object is o, then selected objects is [o]. The first two are single items so don't need to be defined as a list? But I'm confused because I thought the "for o in bpy.context.scene.objects" would automatically be handling this. Wouldn't that just do that for every single object? And then you chose to skip objets if they 
    ctx['object'] = o
    ctx['active_object'] = o
    ctx['selected_objects'] = [o]
    ctx['selected_editable_objects'] = [o]

    bpy.ops.object.shade_smooth(ctx)

    o.data.use_auto_smooth = True   
    o.data.auto_smooth_angle = smooth_radians

# I was running actions on the object by changing to edit mode, merging, then returning to object mode, but you are:
    bm = bmesh.new() #creating a new bmesh, which is just a python memory only mesh object?
    bm.from_mesh(o.data) #change the bmesh object to be the same as the .data from the current object
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=MergeThreshold) #remove doubles, not sure I understand why you need more information than the distance here, and the documentation wasn't much help in me understanding those other values. 
    bm.to_mesh(o.data) #push the bmesh data back to the object data
    bm.free() #delete the bmesh data to free up that memory.

#let me know if I got something there incorrect. I'm double checking as much as possible with the Python API. 

Again, thank you so much. This worked great on the rhino data file. Hitting undo Immediately crashed, but that’s ok. I would save before running something like this anyway. Also that guy said his 3dm importer isn’t quite finished yet. I’d like to share this code with him, if that’s ok with you, to build into his plugin so it does this automatically on every import. Although, he would probably want to put it as a checkbox. Just in case. If there are small holes all over a mesh, this will not work well at All because the mesh won’t be good coming from Rhino unless you make it Really Really dense. It basically ends up looking like what you would expect if you take a complex object, then do a bunch of small booleans all over the place. It’s just never going to be pretty. There are always a few curved ngons that happen, and the triangles that makes won’t be pretty. As soon as you change anything about that model, it goes to hell.

sure, let me break down your questions into relevant chunks:

import bpy, bmesh

Someone mentioned that the import commands aren’t needed any longer?

Not sure who said that or why, but it’s python and python needs imports to be able to work correctly.

from math import radians 

Yeah, I calculated the radian number ahead of time rather than using the math command. Since the angle threshold isn’t actually All that important to be accurate I thought it might actually run faster if I just gave it a number. But you’re right, this is more understandable. When I go back to it, it won’t just be some random looking number later on.

It will technically run faster if you just give it a number, but we’re talking like nanoseconds here. Readability should always be factored into the code you write because confusing code leads to inefficient code (or errors), this is the basis of the “Readability Counts” commandment in the pep guidelines. Being able to come back and understand exactly what your code is doing even years later is also very important.

ctx = bpy.context.copy() 

ctx variable creation, ok, you’re avoiding writing bpy.context over and over. I like that, but I don’t understand the copy() part.

Ok so this one is Blender specific. when you run copy() on a context type object you create a dictionary copy of it, which you can use to modify and pass into operators as an override. Say for example, you have a script that runs an operator that will throw an invalid context exception if you don’t launch it from a 3D viewport. With context overrides, you can override your context.area to point to another area, even if it isn’t active in the current context.

Here’s another area related example- you’re modeling in the 3D viewport and you want to pack your UVs without going over to the UV area for some reason. With a context override this is possible. I’m using it your script to override the selection so the operator thinks certain objects are selected even though they are not. This avoids us having to go in and call object.select_set(True/False) on all of the various objects we need to work on (and then restore whatever the user’s selection was at the end).

for o in bpy.context.scene.objects:
    if not isinstance(o.data, bpy.types.Mesh):  
        continue 

It Looks to me like you made a function named isinstance, but I don’t understand how that works. isinstance doesn’t exist already right, so you’ve named it that? I found it being used once in the Python API documentation, but only in an example code without any explaination. It seems like it’s looking at the current object’s data, and also checking if it is a mesh. I’m not understanding this one, and can’t find results with google that help me understand it. ifinstance( but how do I know what goes here?)

Actually, isinstance() is a built in python function. You give it an object as your first parameter, and a type as your second parameter and it will return either True or False. You can also use a tuple of types as the second parameter if you need to check multiple types. For example, if I need to see if an element is a vert, edge, or face, I would do something like this: isinstance(my_element, (bmesh.types.BMVert, bmesh.types.BMEdge, bmesh.types.BMFace))

    ctx['object'] = o
    ctx['active_object'] = o
    ctx['selected_objects'] = [o]
    ctx['selected_editable_objects'] = [o]

    bpy.ops.object.shade_smooth(ctx)

There’s not much about this block I understand. This creates a context override to handle the different types of objects, like if it’s selected, editable, active, etc. But I don’t understand how the brackets work, and why the = o, then on the other two = [o]. I don’t fully understand what brackets are doing in python yet though either. Best I can find is that it brackets hold lists. But I’m confused if you are saying, an object is o, then an active object is o, then selected objects is [o]. The first two are single items so don’t need to be defined as a list? But I’m confused because I thought the “for o in bpy.context.scene.objects” would automatically be handling this. Wouldn’t that just do that for every single object?

This is where the operator override that I mentioned above comes into play. We are overriding the relevant parts of the context that object.shade_smooth() is expecting. In this scenario, we are looping over every object in the scene and handling each object one by one, so on each loop we are changing the context override to point at the current object. selected_objects and selected_editable_objects are both expected to be lists, so that is why you see the object in brackets.

I was running actions on the object by changing to edit mode, merging, then returning to object mode, but you are:

  • creating a new bmesh, which is just a python memory only mesh object?
  • change the bmesh object to be the same as the .data from the current object
  • remove doubles, not sure I understand why you need more information than the distance here, and the documentation wasn’t much help in me understanding those other values.
  • push the bmesh data back to the object data
  • delete the bmesh data to free up that memory.
    bm = bmesh.new()
    bm.from_mesh(o.data) 
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=MergeThreshold) 
    bm.to_mesh(o.data) 
    bm.free()

Yes, under the hood the built-in operator to remove doubles actually just does what I’ve written here (it actually uses bmesh.ops.remove_doubles). Notice that I’m calling the bmesh operator for remove doubles, not the mesh operator with the same name, this is why the parameters are different. the bmesh operator needs to know what bmesh it’s operating on (there could be many you’ve created), and which verts should be considered.

The main difference between what I’ve done and what you were doing is that simulating user actions (actually changing edit/object mode, updating selections, etc), has a huge amount of overhead. You might not notice it on a small scene with a simple script- but something like this where you’re importing hundreds of objects with nearly a million triangles, you will definitely notice. This is one of the reasons bmesh was created- it allows us to modify meshes without the overhead of having to make our scripts pretend to be users.