Modal operators in Blender



To illustrate how to create a modal operator I created a small add-on, plumb_line, to illustrates a few key principles.

The add-on itself is a bit of a toy, allowing the user to move the 3d cursor around while showing the distance to an intersection point directly below it.

This is not very useful in itself, but it does

  • implement a modal operator
  • show how to create overlays in the 3d view, and
  • how to use Object.ray_cast() and Scene.ray_cast() to find intersections

And because the code covers all kinds of functionality, we structured the code into separate modules, so this add-on also implements some tricks to force module reloading on reinstalling the add-on to prevent having to restart Blender every time we change something. It also touches briefly on using numpy, and offers a lot of configuration options in the user preferences.

Quite a lot for a demo add-on, but in this article we focus on the modal operator.

Anatomy of a modal operator

Not all modal operators look the same, but a common pattern is shown below:

class OBJECT_OT_my_modal_operator(bpy.types.Operator): bl_idname = "object.modalop" bl_label = "Modal Operator" @classmethod def poll(cls, context): ... def modal(self, context, event): if some_condition: ... return {"RUNNING_MODAL"} else: ... return {"FINISHED"} # or CANCELED def invoke(self, context, event): ... context.window_manager.modal_handler_add(self) return {"RUNNING_MODAL"} def cancel(self, context: Context) -> None: ...

The poll() method has the same function as in a non-modal operator, but typically there is no execute() method.

If an operator is invoked, for example from a menu entry, its default invoke() method will typically call its execute() method.

In a modal operator we override the invoke() method, and instead of calling execute() we add a modal handler to the window manager and return "RUNNING_MODAL".

This will cause the window manager to call the modal() method on the handler, typically the operator itself, and keep on doing so as long as that method returns "RUNNING_MODAL".

The window manager will call the the modal() method each time an event occurs, and passes this event as an argument to the call. This event can be anything from a key press to a mouse move and we can even cause timer events to be passed if we enable a timer in the window manager.

This arrangement makes it possible to create interactive add-ons where the user uses keyboard or mouse actions to work with objects in a scene, until they end the interaction, typically by pressing escape or with a right mouse click.

The plumb_line invoke() method

Our operator will be invoked from the Object menu and it needs to do a few things:

  • position the the 3d cursor somewhere above the active object,
  • add a modal handler, and
  • add a pair of draw handlers that show the line and intersection point as well as some text with the distance.

It does a few other things as well, but the relevant lines of code look like this:

def invoke(self, context, event): ... z = max_world_z_of_bounding_box( context.active_object.bound_box, context.active_object.matrix_world ) context.scene.cursor.location = context.active_object.location.copy() context.scene.cursor.location.z = z + 3 # arbitrary offset ... context.window_manager.modal_handler_add(self) ... self.post_view_handler = bpy.types.SpaceView3D.draw_handler_add( draw_handler_post_view, (), "WINDOW", "POST_VIEW" ) self.post_pixel_handler = bpy.types.SpaceView3D.draw_handler_add( draw_handler_post_pixel, (), "WINDOW", "POST_PIXEL" ) return {"RUNNING_MODAL"}

It uses a helper function defined elsewhere to calculate the highest z-coordinate of the active object's bounding box and then copies the location to keep the cursor centered above the object in the x and y directions, but sets its z-coordinate to whatever we calculated plus an arbitrary offset, 3 in this case. Note that we needed to copy the object location because assigning it to the cursor location would also cause the object location to change every time the cursor location is updated as they would refer to the same Vector object.

Next, we add a modal handler, in this case the operator itself. This will cause the window manager to calle the modal() method and we will look at that in the next section.

Finally, we install two draw handlers, also defined in a separate module. The post view draw handler works in 3d space, and will show the actual 'plumb line' going from the 3d cursor to the intersection with the active object. The post pixel draw handler works in 2d on top of everything, and is used to show the text with the measured distance and the intersection highlight. We might cover these handlers in a future blog post.

The return value indicates that we are not done yet, and want to keep on running.

The plumb_line modal() method

def modal(self, context, event): context.area.tag_redraw() if event.type in {"RIGHTMOUSE", "ESC"}: self.cancel(context) return {"CANCELLED"} ... if event.type in {"UP_ARROW", "DOWN_ARROW", ...}: if ( event.value == "PRESS" ): ... context.scene.cursor.location.y += increment ... # calculate the intersection with the object below the cursor context.window_manager.target = worldspace_location context.window_manager.distance_label = ... if ( event.type.find("MOUSE") >= 0 or event.type.find("NUMPAD") >= 0 ): return {"PASS_THROUGH"} return {"RUNNING_MODAL"}

The first thing we do is to make sure that we mark the area for redraw, so that no matter what we do, our draw handlers will be executed.

Next we check if the event was a press of the escape key or a right mouse click, as these are the common way in Blender to interrupt an operation. If it was, we call the cancel() method to remove our draw handlers and return "CANCELLED" to signal that we are done.

We look for some other key presses as well, but we skip that here, but we also look for arrow keys. If such a key was pressed, so not released because we do not want to process a key click twice, we calculate how the position of the 3d cursor should change and the update the cursor location. By looking for just key presses, as opposed to key releases, the user can also keep the key pressed and cause rapid repetition.

With the new cursor location we then calculate the point of intersection with object straight below the cursor. We don´t show that code here, we might cover that in a future blog article, but the result ends up in the worldspace_location variable. We store that in the target property of the window_manager where it will be picked up by our draw handlers to draw a line from the 3d cursor to that point. We also calculate the distance and store that in a window manager property too, to be picked up by our other draw handler that will display this in a text label next to the line.

Because we also want to give the user the option to navigate the 3d view to get a different perspective, so we also check if the event was a mouse event or numpad key, in which case we return "PASS_THROUGH". This will not end our operator but cause the event to pass to Blender's regular event processing, which will move the view around accordingly.

Any other events are simply ignored and we return "RUNNING_MODAL" to keep the operator running.

You may have noticed that none of the paths through the code ever return "FINISHED" and that is intentional: This add-on doesn´t change anything in the scene, just displays information in an overlay, so there is no distinction between canceling an operation or finishing it. That is also why we didn't add "UNDO" to the bl_options variable, as there will never anything to be undone. (In fact, we didn´t define bl_options at all, but its default is just {"REGISTER"})

Code availability

The full code, with full type annotation and lots of additional information in the comments and a readme is available on GitHub.

More ...

If you like these kind of detailed explanations, keep an eye out for future blog posts, or have a look at the Blender add-on development playlist on my YouTube channel.

And if you really, really, like it (and can afford it), you might consider leaving me a tip on Ko-Fi.

Blender add-on development: overlays and user preferences

There are new videos available in the video series on Blender add-on developments for beginners:

 

It is completely free and has a GitHub repository with code .

In this module we will build an add-on that will show the distances between objects as lines and labels in an overlay. The first video will discuss draw handlers, while the other videos will make use of them to create a complete add-on, one that also includes user preferences to choose line color and font size.

Blender add-on development for beginners: Rigging a curve

There are new videos available in the video series on Blender add-on developments for beginners:

 

It is completely free and has a GitHub repository with code .

The new videos are about rigging a curve, that is, creating an armature with bones for every control point in a Bézier curve and then adding hook modifiers to let the curve follow the armature. This setup creates very flexible pose options for things like tentacles and cables etc. and is tedious to do by hand. The add-on solution is not quite obvious, so I also show how to discover some things about curve and splines from the Python console and where to find stuff in the Blender API documentation.

Blender add-on development for beginners: Skinning an armature

There are new videos available in the video series on Blender add-on developments for beginners:

 

It is completely free and has a GitHub repository with code .

The new videos are about skinning an armature, that is, creating a mesh that wraps an armature and is also deformed by that armature. As you will discover, this requires surprisingly little code!

The serie is for beginner add-on developers, but it is still coding of course, so you need to know a bit of Python already. I have tried to keep the Python code simple and readable, and we avoid nerdy stuff as much as possible.

The videos demonstrate how to build working add-ons from scratch.  They are not necessarily very useful in and of themselves, but they show all kinds of relevant concepts and building blocks that are needed in any add-on, and that can be used in your own add-ons. By the end of the first module you should already be able to create an add-on that creates a functional menu item that performs an action on the active object. And even better, you will see that this requires only a few lines of code because Blender's Python API is very well thought out and very powerful: everything you can do as a user can be done in Python as well (and more!), and links to relevant parts of the docs are provided in the video descriptions.

If you like the series and can afford it, consider leaving me a tip on Ko-Fi. Feedback and suggestions are just as welcome, so leave any remarks or ideas in the video comments and/or create an issue in the repository. The idea is to use this feedback to create more videos in the future.


Blender add-on development for beginners - Adding a mesh with operators

There are two new videos available in the video series on Blender add-on developments for beginners:

It is completely free and has a GitHub repository with code .

The new videos are about adding a custom mesh object to your scene. There are multiple ways of doing that and last week we discussed the concepts and demonstrated how to do things from scratch, while in this week's installments we will look at using built-in operators and modifiers.

The serie is for beginner add-on developers, but it is still coding of course, so you need to know a bit of Python already. I have tried to keep the Python code simple and readable, and we avoid nerdy stuff as much as possible.

The other videos demonstrate how to build working add-ons from scratch.  They are not necessarily very useful in and of themselves, but they show all kinds of relevant concepts and building blocks that are needed in any add-on, and that can be used in your own add-ons. By the end of the first module you should already be able to create an add-on that creates a functional menu item that performs an action on the active object. And even better, you will see that this requires only a few lines of code because Blender's Python API is very well thought out and very powerful: everything you can do as a user can be done in Python as well (and more!), and links to relevant parts of the docs are provided in the video descriptions.

If you like the series and can afford it, consider leaving me a tip on Ko-Fi. Feedback and suggestions are just as welcome, so leave any remarks or ideas in the video comments and/or create an issue in the repository. The idea is to use this feedback to create more videos in the future.


Blender add-on development for beginners: adding a mesh

There are two new videos available in the video series on Blender add-on developments for beginners:

It is completely free and has a GitHub repository with code .

The new videos are about adding a custom mesh object to your scene. The are multiple ways of doing that and this week's videos discuss the concepts and demonstrate how to do things from scratch, in next week's installments we will look at using built-in operators and modifiers.

The serie is for beginner add-on developers, but it is still coding of course, so you need to know a bit of Python already. I have tried to keep the Python code simple and readable, and we avoid nerdy stuff as much as possible.

The other videos demonstrate how to build working add-ons from scratch.  They are not necessarily very useful in and of themselves, but they show all kinds of relevant concepts and building blocks that are needed in any add-on, and that can be used in your own add-ons. By the end of the first module you should already be able to create an add-on that creates a functional menu item that performs an action on the active object. And even better, you will see that this requires only a few lines of code because Blender's Python API is very well thought out and very powerful: everything you can do as a user can be done in Python as well (and more!), and links to relevant parts of the docs are provided in the video descriptions.

If you like the series and can afford it, consider leaving me a tip on Ko-Fi. Feedback and suggestions are just as welcome, so leave any remarks or ideas in the video comments and/or create an issue in the repository. The idea is to use this feedback to create more videos in the future.


On the usefulness of python type annotations in Blender add-ons

Yes, this is a bit of a rant, and my opinion may be a bit controversial, but I think Python type annotations are overrated when used in Blender add-on development, and perhaps even in general. Let me explain.

Annotation

The idea of annotation in Python is to add extra information to anything, be it a variable, function parameter or return type, or whatever.

One of its uses is to add type information 1 so that static type checkers can verify that variables are assigned values of a suitable type and IDEs can show additional information about a parameter for example 2.

Because Python is a dynamically typed language this is not enforced at runtime, but exactly because Python is dynamically typed, adding extra information on what kind of types to expect can be incredible useful to let IDEs catch potential problems while writing your code.

For simple functions/methods with parameters of simple types, this is quite clear:

def multiply(a:float, b:float) -> float: return a * b

If we would accidentally pass a string to this function somewhere in our code, the IDE would complain. It is also quite readable, but that starts to change once we are dealing with more complex types:

from typing import Any, Iterable def convert(a: list[Iterable[Any]]) -> tuple[tuple[Any, ...], ...]: """ Convert a list of iterables to a tuple-of-tuples. """ return tuple([tuple(it)for it in a])

Not only do we have to import some types explicitly, it already starts to become difficult to parse for human brains, which goes a bit against the spirit of the Zen of Python: "Readability counts.".

Properties

In Blender we have another issue that goes beyond readability, and that is to use of annotations to describe properties. Take a look at this code snippet:

VScode is not happy. Its typechecker, Pylance, reports "Call expression not allowed in type expression". And that is true for the typing specs, but Blender uses annotations for something completely different here: amount is not simply a float but a property that can hold a float value and at the same time knows how to display itself to the user, what its default is, and a lot more.

So the annotation here is used not for type checking, but for defining both the base type of the property and its behavior, something that couldn´t be described with a simple type annotation like amount: float

There is a solution of sorts for this, and that is to inform the type checker to disregard the annotation as a type hint:

Yes, this does get rid of the red squiggly line, but as you can see from the example() method, it also blocks type checking: We can now happily assign a float to amount which would cause havoc at runtime.

More type checking

Now type checking does have its benefits and if you don´t have to look at the type annotations yourself but let your IDE do the hard work, it arguably is beneficial to have libraries with comprehensive type annotations. The bpy module does not, and as far as I know there are no plans to add this, but some people have stepped in to fill the gap.

The fake-bpy-module provides a stub implementation that you can install inside a virtual environment to develop your Blender code outside of Blender while having the full benefits of comprehensively type annotated code.

It shouldn´t be confused with Blender-as-a-module which lets run Blender from Python directly for use in studio pipelines etc; fake-bpy-module doesn´t contain implementation code, it just provides type annotations. So when you have it installed in your environment and have import bpy, your IDE knows all about the types, but you couldn´t run your code outside Blender. But would you run your code inside Blender, this import would get Blender's bundled bpy module and run just fine.

Now this is great, but be aware that this isn´t all teddy bears and unicorns: You still have the property annotation issue and sometimes the choices made in the fake-bpy-module aren't very convenient:

When I typed in def execute( in vscode, it auto expands the definition based on the type annotation of the execute() method it finds in the Operator class. At first glance this is fine, however, the return type annotation is now a copy of the type annotation of the execute() method in the superclass (i.e. Operator). So if that annotation would change, perhaps because a new possible status string would be added, we (and our IDE) wouldn´t know we could use that when we change our own code.

This apparently happens because the return type is not annotated with a class or type alias, but with a set, and this set definition is what the IDE copies:

def execute( self, context: Context ) -> set[bpy.stub_internal.rna_enums.OperatorReturnItems] ...

I am no expert and I have to think about how this could be fixed (if it can be fixed at all) before submitting a PR perhaps, but Blender's Python API is large and complex, so I don´t blame the module maintainer at all, on the contrary, their efforts for the community are greatly appreciated.

Conclusion

Should we use type annotation in Blender addons? Probably, but not everywhere.

I would argue that type annotations are a benefit if you can factor out functions, which is a good idea for (unit)testing anyway. For top level stuff, like defining an Operator class and such, you do not gain much. I simply keep an execute() function as short as possible and define the actual functionality in separate modules/functions. You can then even put # type: ignore at the top of the file with the operator definition and registration code for minimal annoyance. With this setup you even avoid the property issue: they don´t get flagged when passed to a function defined elsewhere.

In the end it's all up to personal preferences of course, but if you want to use an IDE and not disable type checking altogether, you will have to make some choices.

1: Some people argue that this is its only use, but they are clearly misguided: From the Python glossary "...used by convention as a type hint" (emphasis is mine). And this is important for Blender add-ons, see the section on properties.

2: Among other things: Python compilers, like pypy and others use it to generate optimized code for example.

Blender add-on development for beginners: A video series

Hi everybody,

as you might know I more or less stopped all my commercial work on Blender add-ons, but that doesn´t mean I stopped caring about Blender. So I started working on a video series on Blender add-on developments for beginners:

It is completely free and has a GitHub repository with code .

The first video in the series is an introduction that explains what we are going to cover and what you need to know before you start: The serie is for beginner add-on developers, but it is still coding of course, so you need to know a bit of Python already. I have tried to keep the Python code simple and readable, and we avoid nerdy stuff as much as possible.

The other videos demonstrate how to build working add-ons from scratch.  They are not necessarily very useful in and of themselves, but they show all kinds of relevant concepts and building blocks that are needed in any add-on, and that can be used in your own add-ons. By the end of the first module you should already be able to create an add-on that creates a functional menu item that performs an action on the active object. And even better, you will see that this requires only a few lines of code because Blender's Python API is very well thought out and very powerful: everything you can do as a user can be done in Python as well (and more!), and links to relevant parts of the docs are provided in the video descriptions.

If you like the series and can afford it, consider leaving me a tip on Ko-Fi. Feedback and suggestions are just as welcome, so leave any remarks or ideas in the video comments and/or create an issue in the repository. The idea is to use this feedback to create more videos in the future.


UPDATED - Blempy: easy and fast access to Blender attributes

In a previous post I mentioned future improvements for the blempy package, and the future is now 😀



I also moved the code to its own repository for easier maintenance, and created quite extensive documentation as well.

I am not going to reproduce all that text here, suffice to say that I implementated the unified attribute access I hinted at previously, so you can for example transparently access vertex color data in a few lines of code:


proxy = blempy.UnifiedAttribute(mesh, "Color")
proxy.get()
for polygon_loops in proxy:
    polygon_loops[:,:3] *= 0.5
proxy.set()

(This code will reduce the vertex color brightness by a half for all polygons in the mesh object)

Check the documentation for some more examples.