1
$\begingroup$

I am trying to understand why storing a value on a modal invoke doesn't always work if not creating a class property. The following example works for some properties (focal length for instance) but the stored camera location somehow gets updated with the latest camera location before cancelling. I thought INVOKE was only run once, at the start of the modal.

This modal will set the camera position to 0 and its focal length to 10mm when pressing the left mouse button. Pressing ESC or right-click will cancel the modal and revert the focal length properly, but won't place the camera back to where it was.

class MYADDON_Modal(bpy.types.Operator):
    bl_idname = "myaddon.modal"
    bl_label = "Modal"
    bl_options = {'REGISTER', 'UNDO'}

    def modal(self, context, event):
        cam_obj = context.scene.camera

        if event.type == 'LEFTMOUSE' and context.space_data.type == 'VIEW_3D':
            if event.value == 'PRESS':
                cam_obj.location = (0,0,0)
                cam_obj.data.lens = 10

        # Cancel Modal with RightClick and ESC
        if event.type in {'RIGHTMOUSE', 'ESC'}:
            cam_obj.location = self.stored_cam_loc
            cam_obj.data.lens = self.stored_lens

            if self.cursor_set:
                context.window.cursor_modal_restore()
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        self.stored_cam_loc = context.scene.camera.location
        self.stored_lens = context.scene.camera.data.lens

        context.window.cursor_modal_set('EYEDROPPER')
        self.cursor_set = True

        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

In order to make it work, I have to create a class property by inserting this line before the def modal

stored_cam_loc: bpy.props.FloatVectorProperty()

Could someone explain why this is necessary and what the difference is with other properties?

$\endgroup$
1
  • $\begingroup$ The : means it's an annotation, not a regular attribute that you would define with =. I don't know the internal specifics, but I gather in the registration process these annotations are used to define instance-specific attributes that are correctly linked to the whole API ecosystem. I'm not sure this is your actual question, but you do have to define custom operator properties this way if you want to access them correctly inside the execution code. $\endgroup$
    – Gorgious
    Nov 25, 2021 at 13:51

1 Answer 1

1
$\begingroup$

You don't need a class property and you don't need a bpy.props.... The problem is that in your invoke function you have the line

self.stored_cam_loc = context.scene.camera.location

and that is really assigning stored_cam_loc to be an alternative name for ...camera.location, so that when you change ...camera.location in modal you're actually changing both values.

You can experiment with this annotated version of your class to see this in action. Change True to False at line 42 and you can see print statements showing that the value of both change when the assignment at line 21 is executed.

import bpy
from mathutils import Vector

class MYADDON_Modal(bpy.types.Operator):
    bl_idname = "myaddon.modal"
    bl_label = "Modal"
    bl_options = {'REGISTER', 'UNDO'}

    stored_cam_loc = Vector((0, 0, 0))
    stored_lens = None

    def modal(self, context, event):
        print(f"M Stored lens = {self.stored_lens} Stored loc  = {self.stored_cam_loc}")
        print(f"  Event {event.type}")
        cam_obj = context.scene.camera

        if event.type == 'LEFTMOUSE' and context.space_data.type == 'VIEW_3D':
            print("L")
            if event.value == 'PRESS':
                print(f"P- Stored lens = {self.stored_lens} Stored loc  = {self.stored_cam_loc}")
                cam_obj.location = (0,0,0)
                cam_obj.data.lens = 10
                print(f"P+ Stored lens = {self.stored_lens} Stored loc  = {self.stored_cam_loc}")

        # Cancel Modal with RightClick and ESC
        if event.type in {'RIGHTMOUSE', 'ESC'}:
            print(f"R Stored lens = {self.stored_lens} Stored loc  = {self.stored_cam_loc}")
            cam_obj.location = self.stored_cam_loc
            cam_obj.data.lens = self.stored_lens

            if self.cursor_set:
                context.window.cursor_modal_restore()

            print("C\n")
            return {'CANCELLED'}

        print("M\n")
        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        self.stored_lens = context.scene.camera.data.lens
        if True:
            self.stored_cam_loc.x = context.scene.camera.location.x
            self.stored_cam_loc.y = context.scene.camera.location.y
            self.stored_cam_loc.z = context.scene.camera.location.z
        else
            self.stored_cam_loc = context.scene.camera.location
        print(f"I Stored lens = {self.stored_lens} Stored loc  = {self.stored_cam_loc}")

        context.window.cursor_modal_set('EYEDROPPER')
        self.cursor_set = True

        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}
    
classes = [
    MYADDON_Modal,
]

def register():
    for c in classes:
        bpy.utils.register_class(c)

def unregister():
    for c in classes:
        bpy.utils.unregister_class(c)


if __name__ == '__main__':
    register()

Here's a version without the extra print statements and if statement that seems to work:

import bpy
from mathutils import Vector

class MYADDON_Modal(bpy.types.Operator):
    bl_idname = "myaddon.modal"
    bl_label = "Modal"
    bl_options = {'REGISTER', 'UNDO'}

    stored_cam_loc = Vector((0, 0, 0))
    stored_lens = None

    def modal(self, context, event):
        cam_obj = context.scene.camera

        if event.type == 'LEFTMOUSE' and context.space_data.type == 'VIEW_3D':
            if event.value == 'PRESS':
                cam_obj.location = (0,0,0)
                cam_obj.data.lens = 10

        # Cancel Modal with RightClick and ESC
        if event.type in {'RIGHTMOUSE', 'ESC'}:
            cam_obj.location = self.stored_cam_loc
            cam_obj.data.lens = self.stored_lens

            if self.cursor_set:
                context.window.cursor_modal_restore()

            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        self.stored_lens = context.scene.camera.data.lens
        self.stored_cam_loc.x = context.scene.camera.location.x
        self.stored_cam_loc.y = context.scene.camera.location.y
        self.stored_cam_loc.z = context.scene.camera.location.z

        context.window.cursor_modal_set('EYEDROPPER')
        self.cursor_set = True

        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}
    
classes = [
    MYADDON_Modal,
]

def register():
    for c in classes:
        bpy.utils.register_class(c)

def unregister():
    for c in classes:
        bpy.utils.unregister_class(c)


if __name__ == '__main__':
    register()
```
$\endgroup$
2
  • $\begingroup$ Thanks for the explanation, this is cleaner. But I'm still confused as to why it would behave differently for stored_lens, it should also have been updated by the modal but wasn't You are working around assigning stored_cam_loc to camera.location by storing each axis separately, but how could I store a single float property then? $\endgroup$
    – chafouin
    Dec 19, 2021 at 21:29
  • 2
    $\begingroup$ It has to do with the type of the object. stored_lens is a primitive, while stored_camera is a class instance. $\endgroup$ Dec 19, 2021 at 21:43

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .