Here’s my solution:
import bpy
from mathutils import Vector
from bpy_extras import view3d_utils
from bpy.types import Operator, Panel, UIList, PropertyGroup
from bpy.props import IntProperty, FloatVectorProperty, EnumProperty, CollectionProperty
class CursorLocations(PropertyGroup):
co = FloatVectorProperty(
name="Coordinate",
description="3D location in world-space",
size=3,
subtype='XYZ'
)
def main(context, event, ray_max=10000.0):
"""Run this function on left mouse, execute the ray cast"""
# get the context arguments
ob = context.object
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 * ray_max)
def ob_ray_cast(ob, 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_ob = matrix_inv * ray_origin
ray_target_ob = matrix_inv * ray_target
# cast the ray
hit, normal, face_index = ob.ray_cast(ray_origin_ob, ray_target_ob)
if face_index != -1:
return hit, normal, face_index
else:
return None, None, None
if ob.type == 'MESH' and ob.dupli_type == 'NONE':
matrix = ob.matrix_world.copy()
hit, normal, face_index = ob_ray_cast(ob, matrix)
if hit is not None:
hit_world = matrix * hit
scene.cursor_location = hit_world
#length_squared = (hit_world - ray_origin).length_squared
#length = (hit_world - ray_origin).length
item = ob.cursor_locations.add()
item.name = str(len(ob.cursor_locations))
item.co = hit_world
class VIEW3D_OT_cursor_locations(Operator):
"""Modal object selection with a ray cast"""
bl_idname = "view3d.cursor_locations"
bl_label = "Cursor Locations"
@classmethod
def poll(cls, context):
return (context.object is not None and
context.object.type == 'MESH' and
context.object.dupli_type == 'NONE')
def modal(self, context, event):
ob = context.object
context.area.header_text_set("LMB to raycast, RMB to revert last, Esc to abort. %i of 8 coordinates..." % len(ob.cursor_locations))
if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
# allow navigation
return {'PASS_THROUGH'}
elif event.value == 'PRESS':
if event.type == 'LEFTMOUSE':
main(context, event)
if len(ob.cursor_locations) >= 8:
return self.end(context)
elif event.type == 'RIGHTMOUSE':
if len(ob.cursor_locations) > 0:
idx = len(ob.cursor_locations) - 1
ob.cursor_locations.remove(idx)
ob.data.update()
else:
return self.end(context)
return {'RUNNING_MODAL'}
elif event.type == 'ESC':
return self.end(context)
return {'RUNNING_MODAL'}
def end(self, context):
context.area.header_text_set()
return {'FINISHED'}
def invoke(self, context, event):
if context.space_data.type == 'VIEW_3D':
ob = context.object
if len(ob.cursor_locations) >= 8:
ob.cursor_locations.clear()
ob.data.update()
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "Active space must be a View3d")
return {'CANCELLED'}
class OBJECT_UL_cursor_locations(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if self.layout_type in {'DEFAULT', 'COMPACT'}:
layout.prop(item, "name", text="", emboss=False)
#layout.prop(item, "co", text="")
layout.label("%.3f" % item.co[0])
layout.label("%.3f" % item.co[1])
layout.label("%.3f" % item.co[2])
# 'GRID' layout type should be as compact as possible (typically a single icon!).
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon_value=icon)
class OBJECT_OT_cursor_locations(Operator):
bl_label = "Cursor Locations"
bl_idname = "object.cursor_locations"
action = EnumProperty(
items=(
('ADD', "Add", ""),
('REMOVE', "Remove", ""),
('CLEAR', "Clear", ""),
('MOVEUP', "Move up", ""),
('MOVEDOWN', "Move down", ""),
('PRINT', "Print to system console", ""),
)
)
poll = VIEW3D_OT_cursor_locations.poll
def execute(self, context):
ob = context.object
locs = ob.cursor_locations
if self.action == 'ADD':
#item = locs.add()
#item.name = str(len(locs)) # random name, can be edited in UI
#item.co = context.scene.cursor_locations
pass
elif self.action == 'REMOVE':
locs.remove(len(locs)-1)
elif self.action == 'CLEAR':
locs.clear()
elif self.action == 'MOVEUP':
pass
elif self.action == 'MOVEDOWN':
pass
elif self.action == 'PRINT':
print()
print("========== Cursor Locations ==========")
for loc in locs:
print(loc.name, loc.co)
return {'FINISHED'}
class OBJECT_PT_cursor_locations(Panel):
"""Description goes here..."""
bl_label = "Cursor Locations"
bl_idname = "OBJECT_PT_cursor_locations"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
ob = context.object
row = layout.row()
row.enabled = False
row.template_list("OBJECT_UL_cursor_locations", "", ob, "cursor_locations", ob, "cursor_locations_index", rows=8)
"""
col = row.column(align=True)
col.operator("object.cursor_locations", text="", icon="ZOOMIN").action = 'ADD'
col.operator("object.cursor_locations", text="", icon="ZOOMOUT").action = 'REMOVE'
#col.menu("", text="", icon="DOWNARROW_HLT")
col.separator()
col.operator("object.cursor_locations", text='', icon='TRIA_UP').action = 'MOVEUP'
col.operator("object.cursor_locations", text='', icon='TRIA_DOWN').action = 'MOVEDOWN'
"""
layout.operator("object.cursor_locations", text="Clear list", icon="X").action = 'CLEAR'
layout.operator("object.cursor_locations", text="Print to system console", icon="CONSOLE").action = 'PRINT'
def draw_func(self, context):
self.layout.operator(VIEW3D_OT_cursor_locations.bl_idname)
def register():
bpy.utils.register_module(__name__)
bpy.types.Object.cursor_locations = CollectionProperty(type=CursorLocations)
bpy.types.Object.cursor_locations_index = IntProperty(default=-1)
bpy.types.VIEW3D_PT_tools_transform.append(draw_func)
def unregister():
bpy.utils.unregister_module(__name__)
del bpy.types.Object.cursor_locations
del bpy.types.Object.cursor_locations_index
bpy.types.VIEW3D_PT_tools_transform.remove(draw_func)
if __name__ == "__main__":
register()
Make sure the properties editor is visible and on object tab. Run it via the “Cursor locations” button in the 3D View T-toolshelf (with mesh object selected and being in object mode). Then Click on the active object.