 Hello and welcome to another episode of Scripting for Artists. My name is Siebelen and in this episode we will talk about modal operators. So the recent free episodes of Scripting for Artists have been part of the OpenUp project. You can read about this on blender.org in the news section. This OpenUp project was all about providing some relief during the corona lockdown and slowly but steadily, at least here in the Netherlands, things are opening up a little bit again. So this means the OpenUp project is also coming to an end. Because this is the last episode for now, I wanted to do something a little bit more complex. So that is why we're looking at modal operators. They are a bit more complex, but they're also very powerful and they open up a whole range of new possibilities for your add-ons. So first of all, what is modal? I looked it up on doc.go and it gives me some information, some useful links. Well useful apparently modal is also kind of fabric, I'm not that interested in it. But the second link was to the Merriam-Webster dictionary and it has all kinds of definitions for modal, for example, of or relating to modality. So let's take a look at that, modality, the quality of state of being modal. Yeah, well, thank you very much. But it also says the classification of logical propositions according to their asserting or denying the possibility, impossibility, contingency or necessity of their content. I still don't quite get it, but it has something to do with logic. Well let's take a look at another dictionary, in this case the Oxford Learner's dictionary. And it says for modality, the particular way in which something exists is experienced or is done. And this starts to look more familiar if you think about edit mode or object mode. It is the particular way in which Blender exists at that moment. For example, pressing G in edit mode will move a vertex whereas in object mode it will move an object. So things can have different meanings depending on the mode that Blender is in. And this is also what a modal operator does. It basically adds another mode to Blender which is then defined by your operator. It can take over your complete Blender interface and swallow up all the user input events that there are, or it can just respond to one or two things and then pass on the rest to Blender so that it behaves as normal. Let's see what that looks like in code so that things become a bit more concrete. And demonstrate how a modal operator works, I've made a small little add-on, it's just a silly little thing and the ingredients will look pretty familiar by now. It has a BL info, it imports BPI, it has the operator and then it registers and unregisters when needed. So of course the interesting bit is inside the operator. It starts in the same way as you're used to with the BL ID name, BL label, the naming is consistent with the ID name, yadda yadda yadda. But instead of an execute it has a modal and an invoke. Now mind you it could also have an execute function, it is not the lack of execute function that makes this modal. The invoke function is called by Blender whenever the operator is run from the user interface, so from a hotkey or a menu item or a button on a panel. And this is the function where you would start the modal operator. This invoke function does two things, it tells Blender's window manager that we should receive events and then by returning running modal, it tells Blender that we want to run modal. And that's the basic of getting a modal operator. Now when there is a window manager event like a mouse move, a click, a key press, anything like that, our modal function will be called. And of course it gets the context, but it will also get information about which event triggered it. So these event objects are documented in the Python API reference. So if you want to find that for the version of Blender that you're using at the moment, just go to help and then Python API reference and that will bring you here. You search for event and then you'll find the bpy.types.event structure. This has all kinds of useful properties that are set by Blender before it sends the event to our code. So for movement of the mouse, we have a few different options. We have mouse previous x, so that's the previous location of the mouse. Combine that with mouse x and then you get a sense of the motion that somebody did rather than the absolute position. There's the mouse region x, which is the region relative horizontal location of the mouse and there's mouse x, which is the window relative horizontal location of the mouse. The most important property here is the type. This tells us what kind of event this is in the first place. It's the left mouse, it's middle mouse, right mouse, keyboard, trackpad events. Here you also have a timer event. This one can be very important if you want to do something every once in a while. So that could be every second, every millisecond, every 30 seconds, just what you need. Back to our code, I want to print some different things based on the type of the event. So if event type is mouse move, then I just print mouse move with the mouse x and mouse y coordinates. If the event type is left mouse, I say left click at x and y, else if the event type is in this set, and this is a quick and easy way to just check for a bunch of different values. So if it's either right mouse or escape, I just say right click stopping. It may also be escape. I don't care if this is fine. Then it says return finished, which tells Blender that we want to stop the modal operator. So at this time, it will no longer be receiving any events. If that does not happen, it returns a running modal just like the invoke function did to indicate that it should still be kept running. Let's see what we can get it to print. So I've installed the add-on and enabled it in the user preferences, as you can see. So here we are in Blender, and I've also put my terminal on the screen so that you can see what it prints. Now, this is still without our operator running. You can see that the menu items and everything is responding to our clicks. This is how Blender normally responds. I pressed F3 to go through the search box, and then I looked for the silly example. I press Enter so you can see that it already received its first mouse move event in the terminal. And when I move the mouse, it keeps going. Remember the difference between the window relative and the region relative coordinates. The 3D viewport is a region. The outliner is a region. The properties panel is a region. So if you want to do something that is relative to the 3D viewport, the region relative coordinates will probably be what you need. Now I'm printing the window relative coordinates. So that means that in the bottom right corner, the coordinates are 0, 0. And in the top right, we get to what is almost the resolution of my desktop. One thing you may have noticed is that Blender doesn't respond anymore to these mouseovers because we swallowed up all the events. Also, when I middle click and drag to change the viewport, it doesn't do anything. But it does show a left click and a right click and then it stops. And now it's back to normal again. Our operator has stopped and Blender responds as usual. One thing you may notice is that one click caused two print statements. So the print isn't exactly correct. It's not a left click. It's the mouse down and then the mouse up event that you receive. Again, the Python API documentation to the rescue, there's a value property that will tell you more about the typo event that you're looking at. Let's add this to our print statement. Here in the code, we know it's about the left mouse so we can leave the left in there, but here we just do event.value at a certain location. And then instead of right click, let's just say event.type, event.value, and we'll still be stopping there. Reload the script, rerun it, the mouse move hasn't changed, but you can see that it's a left press and a left release and then a right mouse press. We can run it again, left press, left release, and when we press escape, we get an escape press and then it's over. So that's the basics of modal operators. Let's start looking at something a bit more useful. We're going to follow what is pretty much in the template. So if you go to the scripting tab in Blender and then templates, Python, you will find a bunch of examples for modal operators. So what we're going to do is follow the view 3D operator, but I'm going to do things in a slightly different way than is in this code. And then you can just make up your mind about which approach you'd like more. First, a little demo of what it does. I've run the code in the template and then press F3, simple view operator, you press enter it starts, you in the top left corner, you can see the offset which is now moving. If I move the mouse in the X direction of the window, it also moves the viewport in the X direction of the world and the same for Y. Now if I rotate, then you can see the difference. I'm moving the X direction and you can see that the movement is still in the world X direction and same for the Y direction. If I right click, we end up where we started off. So that's a cancel. And of course, left click is confirmed. So let's look at the invoke function. It does a bit more than we did in our code. Of course, this only works in the 3D viewport and that's the first thing that's being checked on. If contacts.spacedata.type equals view 3D, then the work is done. Otherwise, a warning is shown and the operator is canceled. Personally, I would flip this into if it's not 3D, give a warning, stop and then indent the whole thing. So now that we know it's a 3D viewport, we can access contacts.spacedata and call it V3D and then we can get the 3D region. So a space is made up of regions and in the 3D viewport case, a region is the 3D view but also a header, maybe a photo, maybe some panels on the sides and we have to have the 3D viewport if you want to change the camera around. Then changing the 3D viewport camera only works if you're actually looking through it. If you're looking through a scene camera, this whole thing has to do something different. It would have to move the scene camera around to fix that. It just switches it over to perspective view. And then finally, we have to store the initial location of the mouse so that we can track the motion of the mouse relative to the point where it was when this operator started. And then we have to remember the original location of the viewport camera so that we can restore that when we right click and it has to be canceled. And then finally, we have the known bit which is registering this operator as a model operator and then returning running model. As a side note, in Python, it's possible to just write stuff to self.something. You don't have to declare it as a property, you can just add a new property like this. So these kind of things would not be stored by blender if you do this on an object. So it's very limited in its use but for storing temporary data on an operator like this, it's fine. Now let's look at the model function and the structure of this is pretty much the same as we did before. We look at different event types and then execute a little bit of code. We can change some things around in this code to make it a little bit easier to understand. This bit is only used down here. So let's move it there. It's the same thing as before. It just gets the 3D viewport region so that the view location that is the camera that is used for the viewport, the location of it is set back to the initial location when we press right mouse or escape. This causes it to cancel. It then clears the header text that is set while we're moving the mouse and then it returns canceled. So that tells blender that this operator is no longer supposed to be running. In the case of the left mouse, all that's done is that the offset text is removed from the header and then blender is told that we're finished because during the interaction, during the mouse move, we're already changing the viewport, confirming doesn't need to do anything because the viewport already changed. And then finally we get to the interesting bit. What should happen when the mouse is moving? This line is already doing a lot. So let's split that up into a few easily digested parts. First of all, it's converting event of mouse X and event of mouse Y to a vector so that we can do some 3D math with it. Of course, the camera location in the 3D viewport is a 3D position. So there's a vector and converting things to a vector makes all those computations a little bit easier. So we have a mouse position, which is the vector consisting of mouse X, mouse Y and zero because we don't have a three dimensional mouse. The amount that the mouse moved by is self.initial mouse minus mouse position. So that is the difference between the starting position of the mouse and the current position of the mouse. And then finally we scale the mouse movement because otherwise you would move at around one blender unit for every pixel that you moved your mouse. So how do you get to this number? Well, that's just trial and error. You first tried without the scaling and then you see that things move way too quickly and then you start scaling it down and you get to something that works for you. Finally, self.executor is called. Normally blender would do this for us and it will still do this when we call this operator from Python. However, there is nothing stopping us from calling execute ourselves as well. We'll take a look at what execute is doing exactly in a moment. And then finally the header text is set to offset with the X, Y and Z coordinates of self.offset. This notation is called percent formatting. It consists of a format string, then a percent and then one or more things that are supposed to go into the format string. So in this case our format string has three fields. They start with a percent, they end with an F to indicate a floating point and point four in there means that we want to have four decimals in the number. So let's take a look at that offset and the execute function because those are the final parts we haven't seen yet. So the offset is a property as we've seen before. It's a float vector property and this is imported here as from bpy props import float vector property. So it's just shorter version of writing bpy.props.float vector property. So my name is offset, the size is three which means that we have three values in our float vector and this is because the viewport camera position is of course three dimensional. And then finally we look at the execute function. Again, it gets the 3D region from the context. What's going on here is a little bit confusing because a property type is float vector property but the value that it produces is not a vector. It is something else. So that means that if we want to do some vector math we first have to convert it to a vector. So that's what's happening here. It's converted to a vector and then it's added to the initial location of the viewport camera. And that new location is then assigned to the view location property of the 3D viewport region. So as a quick overview in the invoke we did some preparatory work. We registered that this is a model operator. Then in the model function when the mouse moved we computed an offset and then in the execute function we add that offset to the initial location of the viewport camera to compute the new viewport camera location. Now I wanted to show you how we could call that execute function from Python. But the problem is that in the console you can see that the operator is there and that we can even pass an offset. But if you try to use it, let's see what happens. If you do it like this then you get an error that a space console object doesn't have an attribute region 3D. And that is because currently when you run it here the context is set to the console. So context.spaceData is a space console and not a space view 3D. In the error message you can see that the execute function is actually called by Blender. Line 18 is the problematic one and that is indeed getting view3D.region3D. A console doesn't have a region3D property so that is the issue here. But it does show the idea behind it that you can have an execute function on there that you can call from Python and then an invoke to get something that is interactive. As I said in the beginning a modal operator takes over all of Blender but it's also possible to do that selectively. Whether an event should be passed through to Blender itself or should be used by the modal operator that's determined by the return value of your modal function. This operator now only responds to mouse motion and we cannot zoom in or out. So let's say we want to allow the scroll wheel up and scroll wheel down to be passed through to Blender so that Blender can take care of the zooming forest and then all the other events that they are still gobbled up. So you can't do shift A to get the add menu for example while you're also moving around. This is quite simple to do. First we have to figure out what the event type is that belongs to the scroll wheel. So either you look that up in the Python API reference or you do like me and you just added a print statement that prints out all the event types that you see. So by that you can figure out that it's wheel up mouse and wheel down mouse. Those are the events we're interested in. Now if the event is of any of these two types we can just return paths through. This tells Blender just use the event as normal. This we could also do for the mouse move. We could do also a by default and then the modal operator would be more like an addition to Blender's regular behavior rather than taking over all of the user interface. And this is up to you to determine what your add-on should do. An example of passing through all the events you can find in the timer example. Also in templates Python and then operator modal timer. Using a timer is basically the same as you've seen before is just adding yet another registration function. And of course when you stop you also have to unregister your timer. This time it's the execute function that returns running modal. So you can start a modal operator not only from an invoke but also from an execute. And here you see self.timer equals window manager of the event timer add. This is the number of seconds between timer events that you want to have and you have to pass the window so that it knows which window your operator belongs to. And then modal handler add and return running modal. And then when you want to cancel your timer you have to tell the window manager instead of event timer add you say event timer remove with the thing that you got from event timer add. And in your modal function you will get an event with typist timer and there you can do all kinds of stuff. What you see here in the modal function is that by default it returns pass through. So this does not get in the way of your blender usage except that it changes the theme all the time so it might get in the way after all. So this is it for this episode of scripting for artists. If you have any questions or remarks please leave a comment below. If you want to get more help or you want to just talk about what are good approaches when it comes to add-ons and Python and scripting pop over to blender.chat is the official blender chat platform and there is the Python channel where you can ask all these kinds of questions as many people helping each other out over there. In any case, this is it for now. See you later.