I am trying to, on load of a blend file, start an Operator that uses modal (and tag_redraw) to draw a text label alongside every mesh in the scene. (I got the drawing code etc. from the python template “Operator Modal Draw”.)
The problem is I don’t know how to smoothly start that process; there are NoneType errors atm.
I can make it happen by just running the operator via F3 in the 3D View, but not from the load_post handler.
Here is my sample code, and the errors follow:
bl_info = {
"name": "A Problem with drawing",
"author": "dbat",
"version": (1,0),
"blender": (4,0), # min vers supported
"location": "before load",
"description": "Asking for help with this."
}
import bpy
import addon_utils
import bgl
import blf
from bpy_extras.view3d_utils import location_3d_to_region_2d as vec_3d_2d
from bpy.app.handlers import persistent
def draw_callback_px(self, context):
# Stupid example to just label all the objects.
print("draw callback runs")
font_id = 0
all_objs = [o for o in bpy.data.objects]
for oj in all_objs:
v3d = context.space_data
rv3d = v3d.region_3d
region = bpy.context.region
vec = vec_3d_2d(region, rv3d, oj.location)
x = vec[0]
y = vec[1]
# draw function
blf.position(font_id, x, y, 0)
blf.size(font_id, 10)
blf.draw(font_id, f"I am {oj.name}")
class ModalDrawText(bpy.types.Operator):
"""Text linked to an object"""
bl_idname = "view3d.modal_text"
bl_label = "Show text in 3D Viewport next to the active object"
def invoke(self, context, event):
# The problem is that context.area is empty on startup, I think.
#if not context.area: return ...? what? How do I rerun invoke on fail?
if context.area.type == 'VIEW_3D':
args = (self, context)
self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
# Never gets to run.
def modal(self, context, event):
context.area.tag_redraw()
if event.type in {'RIGHTMOUSE', 'ESC'}:
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
return {'CANCELLED'}
return {'PASS_THROUGH'}
@persistent
def load_handler(dummy):
# Trying to start the label drawing code after the blend file is loaded.
bpy.ops.view3d.modal_text('INVOKE_DEFAULT')
def register():
bpy.utils.register_class(ModalDrawText)
bpy.app.handlers.load_post.append(load_handler)
def unregister():
bpy.utils.unregister_class(ModalDrawText)
bpy.types.OUTLINER_MT_object.remove(outliner_object_menu_func)
if __name__ == "__main__":
register()
The errors, on loading a file:
register_class(...):
AttributeError: 'NoneType' object has no attribute 'type'
Error: Python: Traceback (most recent call last):
File "/media/donn/sea1tb/Projects/06.Blending/blender_addon_dev/addons/problem-drawing-labels/__init__.py", line 50, in invoke
if context.area.type == 'VIEW_3D':
^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'type'
Location: /home/donn/bin/blenders/blender-4.4.0-linux-x64/4.4/scripts/modules/bpy/ops.py:107
Error in bpy.app.handlers.load_post[1]:
Traceback (most recent call last):
File "/media/donn/sea1tb/Projects/06.Blending/blender_addon_dev/addons/problem-drawing-labels/__init__.py", line 73, in load_handler
bpy.ops.view3d.modal_text('INVOKE_DEFAULT')
File "/home/donn/bin/blenders/blender-4.4.0-linux-x64/4.4/scripts/modules/bpy/ops.py", line 107, in __call__
ret = _op_call(self.idname_py(), kw, C_exec, C_undo)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The problem is that context.area is None. modal_text invoke is called in load_handler, but then immediately fails.
How do I “restart” that operator at some point when Blender is ready? Or is there a way to tell it that, actually, I meant you to go use the 3d view all along?
Thanks!