Blender Python #0001

By telleropnul, February 23, 2020

1
2
3
4
5
bpy.context.object                # Active object (last object selected)
bpy.context.selected_objects      # All selected objects 
context.scene.objects             # All objects in current scene
bpy.data.objects                  # All objects 
bpy.data.meshes                   # All meshes

Optimize slices

check existing bool modifiers individually to see if they change the dimensions or vert count of the depsgraph object, removing bools with no effect.
Would be useful for removing unnecessary bool mods on slices
This could potentially be slow in a dense lookup, so it’s best as a util operator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# Iterate through modifiers
for mod in context.object.modifiers:
 
    # Store the current view state of the modifier
    show_viewport = bool(mod.show_viewport)
 
    # Get the dimensions with the modifier enabled and disabled
    dims = []
 
    for show in [True, False]:
        mod.show_viewport = show
        context.view_layer.update()
        dims.append(context.object.dimensions[:])
 
    # Restore the view state of the modifier
    mod.show_viewport = show_viewport
 
    # If the dimensions changed, the modifier stays
    if any(a != b for a, b in zip(dims[0], dims[1])):
        continue
 
    # Get the vertex count with the modifier enabled and disabled
    verts = []
 
    for show in [True, False]:
        mod.show_viewport = show
        context.view_layer.update()
        deps = context.evaluated_depsgraph_get()
        eva = context.object.evaluated_get(deps)
        mesh = eva.to_mesh()
        bm = bmesh.new()
        bm.from_mesh(mesh)
        verts.append(len(bm.verts))
 
    # Restore the view state of the modifier
    mod.show_viewport = show_viewport
 
    # If the vertex count changed, the modifier stays
    if verts[0] != verts[1]:
        continue
 
    # Otherwise, remove the modifier
    context.object.modifiers.remove(mod)

how do i store contents of bpy.context.scene.cursor.location as values not references?

1
2
cursor = bpy.context.scene.cursor.location[:]
bpy.context.scene.cursor.location = cursor

[:] is a slicing operation, it gives you a new array with the values of the one you do it on
Syntax: [start : end : step]
Step is optional and start and end are the start and end of the array of you leave them blank like [:]
for objects and meshes and such you can use obj.copy()
If you ever come across a situation where neither [:] nor .copy() work, there’s always

1
2
3
import copy
a = something
b = copy.deepcopy(a)

Panel popup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import bpy
from .. ui . main_panel import SOURCEOPS_PT_MainPanel
 
class SOURCEOPS_OT_CallPanel(bpy.types.Operator, SOURCEOPS_PT_MainPanel):
    bl_idname = "sourceops.call_panel"
    bl_options = {'REGISTER'}
    bl_label = "SourceOps Panel"
    bl_description = "Display the SourceOps panel"
 
    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self, width=300)
 
    def execute(self, context):
        return {'FINISHED'}

Copy physical name to logical name:

1
2
for obj in bpy.context.selected_objects:
    obj.data.name = obj.name

Drivers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# create empty driver
driver = empty.driver_add('rotation_euler', 2).driver
driver.expression = "(frame - frame_start) * (2 * pi) / (1 + frame_end - frame_start) * 2"
 
# create frame start variable
frame_start = driver.variables.new()
frame_start.name = "frame_start"
frame_start.targets[0].id_type = 'SCENE'
frame_start.targets[0].id = context.scene
frame_start.targets[0].data_path = "frame_start"
 
# create frame end variable
frame_end = driver.variables.new()
frame_end.name = "frame_end"
frame_end.targets[0].id_type = 'SCENE'
frame_end.targets[0].id = context.scene
frame_end.targets[0].data_path = "frame_end"

Blink all objects’ latest Bevel modifiers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
bl_info = {
    "name" : "ModalTimerTest",
    "author" : "c0",
    "description" : "",
    "blender" : (2, 80, 0),
    "version" : (0, 0, 1),
    "location" : "",
    "warning" : "",
    "category" : "Generic"
}
 
import bpy
 
 
class ModalTimerOperator(bpy.types.Operator):
    """Operator which runs its self from a timer"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"
 
    _timer = None
 
    def modal(self, context, event):
        if event.type in {'RIGHTMOUSE', 'ESC'}:
            self.cancel(context)
            return {'CANCELLED'}
 
        if event.type == 'TIMER':
            # assuming we are in 3D view...
            for obj in bpy.context.scene.objects:
                for mod in reversed(obj.modifiers):
                    #print ("obj " + str(obj.name) + " " + str(obj.type))
                    #print ("mod " + str(mod.name) + " " + str(mod.type))
                    #print ("\n")
                    if mod.type == "BEVEL":
                        mod.show_viewport = not mod.show_viewport
                        break
 
        return {'PASS_THROUGH'}
 
    def execute(self, context):
        wm = context.window_manager
        self._timer = wm.event_timer_add(3, window=context.window)
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}
 
    def cancel(self, context):
        wm = context.window_manager
        wm.event_timer_remove(self._timer)
 
 
def register():
    bpy.utils.register_class(ModalTimerOperator)
 
 
def unregister():
    bpy.utils.unregister_class(ModalTimerOperator)
 
 
if __name__ == "__main__":
    register()
 
    # test call
    bpy.ops.wm.modal_timer_operator()

In Blender 2.8 the class name must match the following convention:
UPPER_CASE_{SEPARATOR}_mixed_case

Where the {SEPARATOR} is two letters denoting the class belonging to a certain type (from which type the class is inherited):
HT – Header
MT – Menu
OT – Operator
PT – Panel
UL – UI list

The class identifier “bl_idname” must match the class name. Examples of valid class names and identifiers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class MYADDON_OT_my_operator(bpy.types.Operator):
    bl_idname = 'myaddon.my_operator'
    ...
 
class MYADDON_MT_my_menu(bpy.types.Menu):
    bl_idname = 'MYADDON_MT_my_menu'
    ...
 
class MYADDON_HT_my_header(bpy.types.Header):
    bl_idname = 'MYADDON_HT_my_header'
    ...
 
class MYADDON_PT_my_panel(bpy.types.Panel):
    bl_idname = 'MYADDON_PT_my_panel'
    ...
 
class MYADDON_OT_my_operator(bpy.types.Operator):
    bl_idname = 'myaddon.my_operator'
    ...
 
class MYADDON_MT_my_menu(bpy.types.Menu):
    bl_idname = 'MYADDON_MT_my_menu'
    ...
 
class MYADDON_HT_my_header(bpy.types.Header):
    bl_idname = 'MYADDON_HT_my_header'
    ...
 
class MYADDON_PT_my_panel(bpy.types.Panel):
    bl_idname = 'MYADDON_PT_my_panel'
    ...

If class name or identifier doesn’t meet the conventions, Blender signals with error: ‘…’ doesn’t contain ‘PT’ with prefix & suffix

 

Python list comprehension

1
new_list = [expression(i) for i in old_list if filter(i)]

is equal to

1
2
3
4
new_list = []
for i in old_list:
    if filter(i):
        new_list.append(expressions(i))

which means

1
self.bound_box = hops_math.coords_to_bounds([v.co for v in self.sources[0].data.vertices if v.select])

is equal to:

1
2
3
4
self.bound_box = []
for v in self.sources[0].data.vertices:
    if v.select:
        self.bound_box.append(hops_math.coords_to_bounds(v.co))

Globals

1
2
3
4
5
6
g_scene_name = ''
def store_scene(context):
    global g_scene_name
    g_scene_name = context.scene.name
 
scene = bpy.data.scenes[g_scene_name]

Blender bounding box

Draws two cubes. Move, rotate, scale (not applied).

1
2
for obj in bpy.context.selected_objects:
     print(obj)

<bpy_struct, Object(“Cube”)>
<bpy_struct, Object(“Cube.001”)

1
2
for obj in bpy.context.selected_objects:
     print(obj.bound_box)

<bpy_float[8], Object.bound_box>
<bpy_float[8], Object.bound_box>

1
2
for obj in bpy.context.selected_objects:
     print(obj.bound_box[0])

<bpy_float[3], Object.bound_box>
<bpy_float[3], Object.bound_box>

You can out a for loop in the for loop for going through the bound box corners, then print the 3 floats that a corner consists of with an f string

1
2
3
4
5
6
7
8
9
10
11
12
for obj in bpy.context.selected_objects:
     for corner in obj.bound_box:
         print(f"({', '.join(str(n) for n in corner)})")
 
(-1.0, -1.0, -1.0)
(-1.0, -1.0, 1.0)
(-1.0, 1.0, 1.0)
(-1.0, 1.0, -1.0)
(1.0, -1.0, -1.0)
(1.0, -1.0, 1.0)
(1.0, 1.0, 1.0)
(1.0, 1.0, -1.0)

Or like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from mathutils import Vector
 
obj = bpy.context.active_object
bbox = [Vector(v) for v in obj.bound_box]
print(bbox)
 
[Vector((-1.0, -1.0, -1.0)),
 Vector((-1.0, -1.0, 1.0)),
 Vector((-1.0, 1.0, 1.0)),
 Vector((-1.0, 1.0, -1.0)), 
 Vector((1.0, -1.0, -1.0)), 
 Vector((1.0, -1.0, 1.0)), 
 Vector((1.0, 1.0, 1.0)), 
 Vector((1.0, 1.0, -1.0))]

F string

F string is a format operator
You can put any variable encased into {} between ” ” so python would use it as a string:

f "{int} str {int}"

Here we use F string curly braces {} to evaluate an expression instead of using a single variable:

print(f"({', '.join(str(n) for n in corner)})")

Twist360.py

def center()

Blender User interface elements

https://docs.blender.org/api/current/bpy.ops.html
https://docs.blender.org/api/current/bpy.types.Space.html https://docs.blender.org/manual/en/latest/interface/controls/buttons/menus.html
https://docs.blender.org/api/current/bpy.types.Panel.html

[!] https://docs.blender.org/api/current/bpy.types.Operator.html

Here is an example how to create a custom Panel in Blender Python:

Here is an example how to create a custom Menu in Blender Python:

Pop-up Menus
Pop-up Menus like the [Q] menu for HardOps are just your normal average bpy.types.Menu menu. They are simply floating because they are not part of a Header or Panel which makes them look like pop-up menus.

Menus are part of the user interface. Menus are dependent on a Space area of the screen (in this case View_3D). Typically a menu option calls a Menu (sub menu) or Operator (action).   Operators can call their own floating Menu like Hard Ops does.

To see all Menus try this in Scripting tab console:

1
bpy.types.VIEW3D_M [TAB]

To see all Panels try this in Scripting tab console:

1
bpy.types.VIEW3D_P [TAB]

HT – Header
MT – Menu
OT – Operator
PT – Panel
UL – UI list

How do I call a (sub) menu from a menu option (or from an operator)

Tip: Use [CTRL-C] to copy the call from a menu option to the clipboard and learn the code.

For example, to call the Unwrap submenu:

1
bpy.ops.wm.call_menu(name="VIEW3D_MT_uv_map")

The ‘custom Menu video’ just before the above video contains the following syntax:

1
2
# call another menu
layout.operator("wm.call_menu", text="Unwrap").name = "VIEW3D_MT_uv_map"

Unbevel

 

Add vertices at Edges intersect

Use Merge + Split Edges and Faces to create vertices at intersects.  Extremely useful when switching from non-destructive workflow to destructive (subdivision modelling) workflow.  For when Knife Cut doesn’t cut it.

 

Boxcutter extraction

“bool extract”
BC starts out as normal rectangle cut.
But it doesn’t cut into the main object at all.
There is no diff bool added to the main object.
Then, we grab all the difference bools that are on the main object (negative space; the holes).
We copy them onto the extract cutter cube.
We change each diff bool on the cutter cube into intersect bools.
We now have a ‘negative’ of the main object; we have captured ‘the holes’.
We can now use this cutter as a cookie cutter to make the same holes as on the original object on other objects.

Problem: This type of extraction only works on 100% parametric models where all holes were cut using modifiers.

Solution:

“make extract”
BC starts out as a normal rectangle cut where we draw a cube ‘into’ the object (extrude, no offset).
However, this time BC uses “make” mode (not to be confused with “join” mode).
This cutter is simply a separate object.
Note: We have to recess the cube below the surface slightly (booleans limitation bypass)
We can now substract the original object from the make cube to capture the negative space, the holes.
The ‘make cube’ cutter has 1 difference boolean (with main object). There are no slices to be transferred.
We can ‘Apply’ this bool if we want stop being dependent on the modifier stack of the main object.
Same as before, we can now use this cutter as a cookie cutter to make the same holes as on the original object on other
objects.

Advantage: “make extract” also captures non-parametric holes e.g. [Edit Mode] subdivision modelling holes.

Source file: make extract.blend

Vertcircle

1
2
3
4
5
6
7
8
    bpy.ops.mesh.bevel(offset=0.1, segments=5, vertex_only=True)
    bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
    # start - remove netting faces from selection
    bpy.ops.mesh.hide(unselected=True)
    bpy.ops.mesh.select_face_by_sides(number=4, type='GREATER', extend=False)
    bpy.ops.mesh.select_all(action='INVERT')
    bpy.ops.mesh.reveal(select=False)
    # end - remove netting faces from selection

vertcircle_fix.blend

vertcircle_nth__test_diamonds.blend