Overriding

Hi,

I have seen quite a few questions regarding this issue, and I have not been able to wrap my head around it.

So here is a concrete example.

I create a cube and in edit mode I add loopcuts. No sweat!

I copy the code from the info window and I try it out in a script and I get the ominous RuntimeError: Operator bpy.ops.mesh.loopcut_slide.poll() expected a view3d region & editmesh error message.

I understand now that it is about first giving python the right context by overriding. And alas, I do not know how to do it!

Here is the code:

import bpy
import os

os.system("cls")

# remove the default cube...
objs = bpy.data.objects
for obj in objs:
    if obj.name.find("Cube") == 0:
        bpy.data.objects.remove(obj, do_unlink=True)

# add a cube!
bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=True, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
# do something to it to be sure that we have it...
bpy.data.objects['Cube'].scale[0] = 10

# THE CODE BELOW GIVES THE RuntimeError: Operator bpy.ops.mesh.loopcut_slide.poll() expected a view3d region & editmesh error message.

# What is the override code I have to use to fix it????????

bpy.ops.mesh.loopcut_slide(MESH_OT_loopcut={"number_cuts":16, "smoothness":0, "falloff":'INVERSE_SQUARE', "object_index":0, "edge_index":4, "mesh_select_mode_init":(True, False, False)}, TRANSFORM_OT_edge_slide={"value":0, "single_side":False, "use_even":False, "flipped":False, "use_clamp":True, "mirror":True, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "correct_uv":True, "release_confirm":False, "use_accurate":False})

first you’ll need to find a reference to the 3d area in your context, then copy the current context and assign it to the area key. After that you can pass it in to an operator.

ctx_override = bpy.context.copy()
ctx_override['area'] = the_area
bpy.ops.mesh.loopcut_slide(ctx_override, ...)

Now, that is a good start.

And, as mentioned, I am so new at this that I need to know more about what you stick in "ctx_override[‘area’] = the_area!

What should “the_area” be in the simple case I mention: adding loopcuts to a cube I just created?

I tried the code you mention in the console, and although I can see the ‘area’, I don’t know what to set it to!

And then, when I am done, I suppose that I have to restore what I have overridden, or does it not make sense?

You’ll need to find a valid area in the current context. I’ll give you a function that will return the first 3d viewport area it finds, but first let me explain how you would do it manually so you understand how it works.

As you know, in blender the UI is made up of a series of areas. all of the areas in the current context (that is, the workspace/tab/layout you are currently viewing) can be found in bpy.context.screen.areas. If you were to loop through this property collection and print out the type in the default Scripting tab of a fresh blender install, you’d see this:

Some operators might even require a specific region in addition to an area. Doing the same loop/print on the VIEW_3D area’s areas would give us this:

So as you can see there’s a bit of an onion effect happening here- the best way to get a handle on the relationships each of these objects have with one another is to start somewhere in the official API docs and just drill down until you’re saturated. At some point it will just “click”.

as promised, here’s a handy script to help you create an overridden context, and an example of how to use it:

import bpy

def find_3d_viewport_area(ctx = None):
    # allow the user to pass in a context, but handle it for them if there isn't one.
    # it is always best to use a local context (ie: from an operator or callback) rather than
    # the global context, but sometimes that's all you've got.
    if ctx is None:
        ctx = bpy.context

    for a in ctx.screen.areas:
        if a.type == 'VIEW_3D':
            return a
    return None

def main():    
    v3d = find_3d_viewport_area()
    if v3d is None:
        raise Exception("There was no 3d viewport! Stopping!")

    # bpy.types.Context's copy() function will return a dictionary. If you're curious what's in it, try printing it out!
    # you could actually build this dictionary manually and pass it in- Blender doesn't care! It's just easier to start from
    # one that already exists since you don't ever really know what it is that an operator's Poll function cares about (this
    # generally means some trial and error is involved while you "guess")

    ctx = bpy.context.copy()

    # uncomment the following two lines to see what's in the context dictionary
    # from pprint import pprint as pp
    # pp(ctx)

    ctx['area'] = v3d
    ctx['region'] = v3d.regions[5] # I happen to know that a VIEW_3D area's fifth region is the window, which is what the view3d.view_all operator is checking for. But we could always create a "find region of type" function similar to the one that found the area.
    
    # and finally, run the built in operator with our context override passed in as the first parameter. Normally, this operator would fail when running from the
    # text editor because the operator's poll would throw an exception since we aren't in the correct area to run it. With this override it's happy.
    bpy.ops.view3d.view_all(ctx)    
    
main()

And then, when I am done, I suppose that I have to restore what I have overridden, or does it not make sense?

there’s nothing to restore since you’re making a copy- bpy operators are fire and forget, so you’re passing in an overridden context and Blender doesn’t care what you do with it afterward, it’s just a dictionary after all.

1 Like

First, thank you so much for your very detailed exposé. You have no idea how much I am learning (for instance I did not know about writing a main routine that way
)

I religiously ran your code one step at a time from the console (after I found out a bug: the fifth element has index 4, not 5, lucky for me that it was the last element).

I end up knowing how to do all that, and yet, I am still missing what I hope to be the last step, namely how the heck do I use it to give my poor cube in the original post some loopcuts!

I know, that shows the extend of my newbieness, which I do not deny, and yet, with the kind of patience people like you show, I am learning a lot


ah, yes- if the HUD region has not been created yet there will be one less region in the 3d view area, i forgot it’s initialized on demand.

Please


I suppose that for you all these things are very simple, and for me, not.

Will you take the few minutes it would require to run the code I originally posted (I repeat it here for your convenience) and add the necessary code so that the loopcuts can be added in python!

import bpy
import os

os.system("cls")

# remove the default cube...
objs = bpy.data.objects
for obj in objs:
    if obj.name.find("Cube") == 0:
        bpy.data.objects.remove(obj, do_unlink=True)

# add a cube!
bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=True, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
# do something to it to be sure that we have it...
bpy.data.objects['Cube'].scale[0] = 10

# THE CODE BELOW GIVES THE RuntimeError: Operator bpy.ops.mesh.loopcut_slide.poll() expected a view3d region & editmesh error message.

# What is the override code I have to use to fix it????????

bpy.ops.mesh.loopcut_slide(MESH_OT_loopcut={"number_cuts":16, "smoothness":0, "falloff":'INVERSE_SQUARE', "object_index":0, "edge_index":4, "mesh_select_mode_init":(True, False, False)}, TRANSFORM_OT_edge_slide={"value":0, "single_side":False, "use_even":False, "flipped":False, "use_clamp":True, "mirror":True, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "correct_uv":True, "release_confirm":False, "use_accurate":False})

Thank you in advance


well, in your situation specifically you also need to be in edit mode to use mesh operators- there’s no cheating that one with contexts, so the solution would be to use the operator to enter edit mode and also pass in your region/view context.

import bpy
import os

os.system("cls")

def find_3d_viewport_area(ctx = None):
    # allow the user to pass in a context, but handle it for them if there isn't one.
    # it is always best to use a local context (ie: from an operator or callback) rather than
    # the global context, but sometimes that's all you've got.
    if ctx is None:
        ctx = bpy.context

    for a in ctx.screen.areas:
        if a.type == 'VIEW_3D':
            return a
    return None

def create_ctx_override():
    v3d = find_3d_viewport_area()
    if v3d is None:
        raise Exception("There was no 3d viewport! Stopping!")

    ctx = bpy.context.copy()
    ctx['area'] = v3d
    ctx['region'] = v3d.regions[5]

# remove the default cube...
objs = bpy.data.objects
for obj in objs:
    if obj.name.find("Cube") == 0:
        bpy.data.objects.remove(obj, do_unlink=True)

# add a cube!
bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=True, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
# do something to it to be sure that we have it...
bpy.data.objects['Cube'].scale[0] = 10

# THE CODE BELOW GIVES THE RuntimeError: Operator bpy.ops.mesh.loopcut_slide.poll() expected a view3d region & editmesh error message.

# What is the override code I have to use to fix it????????

bpy.ops.object.editmode_toggle()
ctx = create_ctx_override()

bpy.ops.mesh.loopcut_slide(ctx, MESH_OT_loopcut={"number_cuts":16, "smoothness":0, "falloff":'INVERSE_SQUARE', "object_index":0, "edge_index":4, "mesh_select_mode_init":(True, False, False)}, TRANSFORM_OT_edge_slide={"value":0, "single_side":False, "use_even":False, "flipped":False, "use_clamp":True, "mirror":True, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "correct_uv":True, "release_confirm":False, "use_accurate":False})

Thanks a lot!

I did more homework in the mean time and I found a similar answer on the following thread: https://blender.stackexchange.com/questions/43060/loop-cut-and-slide-using-python with a small correction on this one: https://blender.stackexchange.com/questions/160739/loop-cut-and-slide-using-python-not-working/164563#164563.

Since then I found another issue I have not solved yet:

I do not know on which faces the loopcuts will come when I do it with python.
There is a parameter called edge_index that seems to control it, and I have not found a description of how to use it.

I have created the following script to try to investigate the issue, and there is too much data for me to have a conclusion I can use. Here is the script:

# this override code works!
# From https://blender.stackexchange.com/questions/43060/loop-cut-and-slide-using-python
# and
# from https://blender.stackexchange.com/questions/160739/loop-cut-and-slide-using-python-not-working/164563#164563

import bpy
import time
import os
import pprint

os.system("cls")

def view3d_find( return_area = False ):
    # returns first 3d view, normally we get from context
    for area in bpy.context.window.screen.areas:
        if area.type == 'VIEW_3D':
            v3d = area.spaces[0]
            rv3d = v3d.region_3d
            for region in area.regions:
                if region.type == 'WINDOW':
                    if return_area: return region, rv3d, v3d, area
                    return region, rv3d, v3d
    return None, None

region, rv3d, v3d, area = view3d_find(True)

override = {
    'scene'  : bpy.context.scene,
    'region' : region,
    'area'   : area,
    'space'  : v3d
}

##### START OF deselect All Objects FUNCTION
def deselectAllObjects():
    # from https://blenderartists.org/t/element-selected-in-outliner-and-i-dont-want-it-to-be/1296825/3
    for selected in bpy.context.selected_objects:
        selected.select_set(False)
##### END OF deselect All Objects FUNCTION
        
##### START OF selectObjectByName FUNCTION
def selectObjectByName(objectToSelect):
    ## deselect all (just in case?) then select and activate ReferenceThickLine WITHOUT USING bpy.ops
    # from https://blenderartists.org/t/element-selected-in-outliner-and-i-dont-want-it-to-be/1296825/3
    for selected in bpy.context.selected_objects:
        selected.select_set(False)
    newObject = bpy.data.objects[objectToSelect] 
    newObject.select_set(True)
    bpy.context.view_layer.objects.active = newObject
# other code to select only one object, using bpy.ops...
# from https://blenderartists.org/t/element-selected-in-outliner-and-i-dont-want-it-to-be/1296825/3
# don't run it, the one above works too
#bpy.ops.object.select_all(action='DESELECT')
#obj = bpy.data.objects["ReferenceThickLine"] 
#obj.select_set(True)
#bpy.context.view_layer.objects.active = obj
##### END OF selectObjectByName FUNCTION


#### START OF PUTLOOPCUTS(2) FUNCTION
def putLoopCuts(kGiven, numCutsGiven):

    bpy.ops.mesh.loopcut_slide(
        override, 
        MESH_OT_loopcut = {
            "object_index" : 0,
            "number_cuts"           : numCutsGiven,
            "smoothness"            : 0,     
            "falloff"               : 'SMOOTH',
            "edge_index"            : kGiven,
            "mesh_select_mode_init" : (True, False, False)
        },
        TRANSFORM_OT_edge_slide = {
            "value"           : 0,
            "mirror"          : False, 
            "snap"            : False,
            "snap_target"     : 'CLOSEST',
            "snap_point"      : (0, 0, 0),
            "snap_align"      : False,
            "snap_normal"     : (0, 0, 0),
            "correct_uv"      : False,
            "release_confirm" : False
        }
    )
#### END OF PUTLOOPCUTS(2) FUNCTION

#### START OF DELETEALLOBJECTS() FUNCTION
def deleteAllObjects():
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete(use_global=False)
#### END OF DELETEALLOBJECTS() FUNCTION

def main():
    
    numberOfLoopcuts = 3
    deleteAllObjects()
    startTime = time.time()
    print("At start of let's have fun: STARTING at", startTime)
    deselectAllObjects()
    cubeNumber = 0
    for i in range(20):
        for j in range(20):
            bpy.ops.mesh.primitive_cube_add(enter_editmode=True, location = ((j - 0) * 4 + 1, -i * 4 - 1, 0))
            bpy.context.object.name = "cube_" + str(cubeNumber) + "_" + str(i) + "_" + str(j) + "_"
            cubeNumber += 1
            for k in range(i, j + 1):
                print("    126 Doing k from", i, "to", (j + 1), ":", k)
                putLoopCuts(k, numberOfLoopcuts)
    
            bpy.context.object.name += str(k)
            print("120 Doing i:j", i, ":", j, ":", bpy.context.object.name)
            bpy.ops.object.editmode_toggle()

main()

Any ideas!

1 Like

In fact, I just tried to run your code and I still get the same error message when you run

    ctx['region'] = v3d.regions[5]

because v3d has only 5 regions. And when I change the 5 to 4, which had worked before, I get the following error message:

File “C:\Program Files\Blender Foundation\Blender 2.92\2.92\scripts\modules\bpy\ops.py”, line 82, in _parse_args
raise ValueError(“1-3 args execution context is supported”)
ValueError: 1-3 args execution context is supported

Going beyond that to troubleshoot is above and beyond my present abilities


Any idea


right- this is what I mentioned in my previous post, regions are created on-demand, so they are not going to be reliably in one index or another. You will need to write another function to find the window region (basically the same thing I showed you for finding the area- but now regions).

I do not know on which faces the loopcuts will come when I do it with python.
There is a parameter called edge_index that seems to control it, and I have not found a description of how to use it

Edge index is the ‘representative edge’ for the ring of edges the loop will be created on. All mesh components in blender have an index, and you can turn them on in the viewport overlay settings if you have developer extras enabled.

Thank you so much so much for the info about the indices! That way, I precisely know what to do.

And about the v3d, I suppose that I will have to do my homework


And I am uploading a new request for help on an addon I create (the continuation of the code I was asking about here) that crashed Blender and I don’t know what to do. I you have the inclination to look at it


In any case, thanks a lot again.

I have looked at the edge_indices now, and I wonder what the logic is for their assignments when for instance a cube is added.

I can see that if I add my loopcuts so that they intersect a given edge_index number, so that is what the edge_index number I get after the command has been executed.

The question is, how does it pick the edge_index?

It is not the one closest in the view, it is not the first one in number sequence (I have tried both, and it does not seem to matter.)

That makes it difficult to make loopcuts in a script that fall reliably, for instance, as is the case for me, if I want to have loopcuts on the long face of an otherwise thin cube


when you use the loop cut operation it’s picking the edge index using a bvh raycast. the bvhtree’s raycast function doesn’t actually return edges, but face indices- you have to then do edge intersection tests to determine which edge of the face is most relevant to where the mouse cursor is.

you’re teetering on the edge of the rabbit hole, it’s a long way down :slight_smile:

Woah, indeed a deep rabbit hole, and I am glad that you are showing me the entrance. :rabbit2:

What I don’t understand here is that when I set loopcut from a script, I have no idea of where the mouse is, do I, whereas when I do it live, I can see that it is the process that is going on which suggests placing the loopcuts on this or that direction, depending on how I move the mouse.

Thanks

blender’s built-in loopcut operator is modal, rather than fire-and-forget, which gives it an opportunity to do preselect highlighting to show you where your cut will be. It does this with a GL Draw handler. In order to know all of the edges that will be affected by the operation you have to have a pretty solid knowledge of how bmesh works, which is another very deep topic- but the short version is that you start with your raycast edge result, then use bmesh to ‘walk’ the connected bmloops until you’ve either reached a triangle, a boundary, an ngon, or a bmloop that has already been discovered by your search. Once you’ve done that you need to create a GL Batch to draw the lines. It’s definitely one of those things where it’s not very hard if you already know all of the things you need to know, but if you’re starting from scratch it’s a bit of a steep climb.

To understand more about how bmesh works, I’d advise starting here (the design document):
https://wiki.blender.org/wiki/Source/Modeling/BMesh/Design

For the GL module, there are a lot of good examples of how to create batches on the api docs when you get to that point.

Another rabbit hole, he!

Luckily enough, I don’t need such a level for what I do.

A question, though:

When I create say a cube, go in edit mode with indices enabled, can I expect to find those indices at the same location each time, or are they distributed at random?

If it is the first case, as I would expect, then I can take that into account when I want to apply some loopcuts, can’t I?

Thanks

assuming that none of the initialization parameters have changed, the indices should be consistent between operator calls.

I am relieved, then!

Thank you
P.S. If you are in the mood, I uploaded a new question