111 lines
3.6 KiB
Python
111 lines
3.6 KiB
Python
bl_info = {
|
|
"name": "Cursor to Center of Mass + Bounding Box (Multi-Object, Skip Empties)",
|
|
"author": "Ryan & ChatGPT",
|
|
"version": (1, 5),
|
|
"blender": (4, 0, 0),
|
|
"location": "Object → Set Cursor",
|
|
"description": "Adds Object → Set Cursor submenu with Center of Mass (Surface/Volume) and Bounding Box options, works with multiple objects (ignores empties)",
|
|
"category": "3D View",
|
|
}
|
|
|
|
import bpy
|
|
import bmesh
|
|
from mathutils import Vector
|
|
|
|
|
|
def compute_position(obj, mode='SURFACE'):
|
|
"""Return world-space position for different cursor modes for a single object."""
|
|
if not obj or obj.type != 'MESH':
|
|
return obj.location.copy()
|
|
|
|
mesh = obj.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(mesh)
|
|
bm.verts.ensure_lookup_table()
|
|
|
|
if mode == 'SURFACE':
|
|
# Average of vertex positions
|
|
if bm.verts:
|
|
points = [obj.matrix_world @ v.co for v in bm.verts]
|
|
pos = sum(points, Vector()) / len(points)
|
|
else:
|
|
pos = obj.location.copy()
|
|
|
|
elif mode in {'VOLUME', 'BOUNDING_BOX'}:
|
|
# Bounding box center
|
|
bb = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
|
|
pos = sum(bb, Vector()) / 8.0
|
|
|
|
else:
|
|
pos = obj.location.copy()
|
|
|
|
bm.free()
|
|
return pos
|
|
|
|
|
|
class VIEW3D_OT_cursor_set_custom(bpy.types.Operator):
|
|
bl_idname = "view3d.cursor_set_custom"
|
|
bl_label = "Cursor to Custom Position"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
mode: bpy.props.EnumProperty(
|
|
name="Mode",
|
|
description="Where to place the 3D Cursor",
|
|
items=[
|
|
('SURFACE', "Center of Mass (Surface)", "Average of vertices"),
|
|
('VOLUME', "Center of Mass (Volume)", "Approximate using bounding box center"),
|
|
('BOUNDING_BOX', "Bounding Box Center", "Center of the bounding box"),
|
|
],
|
|
default='SURFACE',
|
|
)
|
|
|
|
def execute(self, context):
|
|
# Ignore empties and non-object types
|
|
sel = [obj for obj in context.selected_objects if obj and obj.type != 'EMPTY']
|
|
if not sel:
|
|
self.report({'WARNING'}, "No valid objects selected (ignoring empties)")
|
|
return {'CANCELLED'}
|
|
|
|
# Collect per-object positions
|
|
positions = [compute_position(obj, self.mode) for obj in sel]
|
|
|
|
if not positions:
|
|
self.report({'WARNING'}, "No valid positions found")
|
|
return {'CANCELLED'}
|
|
|
|
# Average across all objects
|
|
cursor_pos = sum(positions, Vector()) / len(positions)
|
|
|
|
context.scene.cursor.location = cursor_pos
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Submenu: Object → Set Cursor
|
|
class VIEW3D_MT_set_cursor(bpy.types.Menu):
|
|
bl_label = "Set Cursor"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.operator("view3d.cursor_set_custom", text="Cursor to Center of Mass (Surface)").mode = 'SURFACE'
|
|
layout.operator("view3d.cursor_set_custom", text="Cursor to Center of Mass (Volume)").mode = 'VOLUME'
|
|
layout.operator("view3d.cursor_set_custom", text="Cursor to Bounding Box Center").mode = 'BOUNDING_BOX'
|
|
|
|
|
|
def draw_set_cursor_menu(self, context):
|
|
self.layout.menu("VIEW3D_MT_set_cursor", text="Set Cursor")
|
|
|
|
|
|
def register():
|
|
bpy.utils.register_class(VIEW3D_OT_cursor_set_custom)
|
|
bpy.utils.register_class(VIEW3D_MT_set_cursor)
|
|
bpy.types.VIEW3D_MT_object.append(draw_set_cursor_menu)
|
|
|
|
|
|
def unregister():
|
|
bpy.types.VIEW3D_MT_object.remove(draw_set_cursor_menu)
|
|
bpy.utils.unregister_class(VIEW3D_MT_set_cursor)
|
|
bpy.utils.unregister_class(VIEW3D_OT_cursor_set_custom)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|