Utility_Apps/Blender/addons/Cursor to Center of Mass + Bounding Box/__init__.py

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()