blender/scripts/templates_py/operator_modal_view3d_raycast.py
2023-09-05 11:04:27 +10:00

116 lines
3.9 KiB
Python

import bpy
from bpy_extras import view3d_utils
def main(context, event):
"""Run this function on left mouse, execute the ray cast"""
# get the context arguments
scene = context.scene
region = context.region
rv3d = context.region_data
coord = event.mouse_region_x, event.mouse_region_y
# get the ray from the viewport and mouse
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
ray_target = ray_origin + view_vector
def visible_objects_and_duplis():
"""Loop over (object, matrix) pairs (mesh only)"""
depsgraph = context.evaluated_depsgraph_get()
for dup in depsgraph.object_instances:
if dup.is_instance: # Real dupli instance
obj = dup.instance_object
yield (obj, dup.matrix_world.copy())
else: # Usual object
obj = dup.object
yield (obj, obj.matrix_world.copy())
def obj_ray_cast(obj, matrix):
"""Wrapper for ray casting that moves the ray into object space"""
# get the ray relative to the object
matrix_inv = matrix.inverted()
ray_origin_obj = matrix_inv @ ray_origin
ray_target_obj = matrix_inv @ ray_target
ray_direction_obj = ray_target_obj - ray_origin_obj
# cast the ray
success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
if success:
return location, normal, face_index
else:
return None, None, None
# cast rays and find the closest object
best_length_squared = -1.0
best_obj = None
for obj, matrix in visible_objects_and_duplis():
if obj.type == 'MESH':
hit, normal, face_index = obj_ray_cast(obj, matrix)
if hit is not None:
hit_world = matrix @ hit
scene.cursor.location = hit_world
length_squared = (hit_world - ray_origin).length_squared
if best_obj is None or length_squared < best_length_squared:
best_length_squared = length_squared
best_obj = obj
# now we have the object under the mouse cursor,
# we could do lots of stuff but for the example just select.
if best_obj is not None:
# for selection etc. we need the original object,
# evaluated objects are not in view-layer.
best_original = best_obj.original
best_original.select_set(True)
context.view_layer.objects.active = best_original
class ViewOperatorRayCast(bpy.types.Operator):
"""Modal object selection with a ray cast"""
bl_idname = "view3d.modal_operator_raycast"
bl_label = "Ray-cast View Operator"
def modal(self, context, event):
if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
# allow navigation
return {'PASS_THROUGH'}
elif event.type == 'LEFTMOUSE':
main(context, event)
return {'RUNNING_MODAL'}
elif event.type in {'RIGHTMOUSE', 'ESC'}:
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.space_data.type == 'VIEW_3D':
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "Active space must be a View3d")
return {'CANCELLED'}
def menu_func(self, context):
self.layout.operator(ViewOperatorRayCast.bl_idname, text="Raycast View Modal Operator")
# Register and add to the "view" menu (required to also use F3 search "Ray-cast View Modal Operator" for quick access).
def register():
bpy.utils.register_class(ViewOperatorRayCast)
bpy.types.VIEW3D_MT_view.append(menu_func)
def unregister():
bpy.utils.unregister_class(ViewOperatorRayCast)
bpy.types.VIEW3D_MT_view.remove(menu_func)
if __name__ == "__main__":
register()