From 8ccc37b9024df21ba66ba2414dc5fa736f2c795a Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Sat, 17 Aug 2024 19:46:57 +0100 Subject: [PATCH 1/2] Rework Origins Pie --- source/pie_origin.py | 404 ++++++++++++++----------------------------- 1 file changed, 127 insertions(+), 277 deletions(-) diff --git a/source/pie_origin.py b/source/pie_origin.py index 133c7f2..02ff85e 100644 --- a/source/pie_origin.py +++ b/source/pie_origin.py @@ -4,319 +4,169 @@ import bpy -from bpy.types import ( - Menu, - Operator, -) +from bpy.types import Menu, Operator +from bpy.props import StringProperty from .hotkeys import register_hotkey -# Pivot to selection -class PIE_OT_PivotToSelection(Operator): - bl_idname = "object.pivot2selection" - bl_label = "Pivot To Selection" - bl_description = "Pivot Point To Selection" +class PIE_OT_set_origin_to_selection(Operator): + bl_idname = "object.origin_set_to_selection" + bl_label = "Origin To Selection" + bl_description = "Snap the origin to the selection" bl_options = {'REGISTER', 'UNDO'} - @classmethod - def poll(cls, context): - return context.active_object is not None - def execute(self, context): + org_mode = 'OBJECT' + if context.active_object: + org_mode = context.active_object.mode saved_location = context.scene.cursor.location.copy() bpy.ops.view3d.snap_cursor_to_selected() bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.origin_set(type='ORIGIN_CURSOR') + bpy.ops.object.mode_set(mode=org_mode) context.scene.cursor.location = saved_location + self.report({'INFO'}, "Snapped the origin point to the selection.") + return {'FINISHED'} -# Pivot to Bottom - - -def origin_to_bottom(ob): - if ob.type != 'MESH': - return - - init = 0 - for x in ob.data.vertices: - if init == 0: - a = x.co.z - init = 1 - elif x.co.z < a: - a = x.co.z - - for x in ob.data.vertices: - x.co.z -= a - - ob.location.z += a - - -class PIE_OT_PivotBottom(Operator): - bl_idname = "object.pivotobottom" - bl_label = "Pivot To Bottom" - bl_description = ( - "Set the Pivot Point To Lowest Point\n" - "Needs an Active Object of the Mesh type" - ) +class PIE_OT_set_origin_to_bottom(Operator): + bl_idname = "object.origin_set_to_bottom" + bl_label = "Origin To Bottom" + bl_description = "Set the Object Origin to the lowest point of each selected object" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): - obj = context.active_object - return obj is not None and obj.type == "MESH" - - def execute(self, context): - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') - - for ob in context.selected_objects: - origin_to_bottom(ob) - - return {'FINISHED'} - - -# Pivot to Bottom -class PIE_OT_PivotBottom_edit(Operator): - bl_idname = "object.pivotobottom_edit" - bl_label = "Pivot To Bottom" - bl_description = ( - "Set the Pivot Point To Lowest Point\n" - "Needs an Active Object of the Mesh type" - ) - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - obj = context.active_object - return obj is not None and obj.type == "MESH" - - def execute(self, context): - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') - - for ob in context.selected_objects: - origin_to_bottom(ob) - - bpy.ops.object.mode_set(mode='EDIT') - - return {'FINISHED'} - - -# Pivot to Cursor Edit Mode -class PIE_OT_PivotToCursor_edit(Operator): - bl_idname = "object.pivot2cursor_edit" - bl_label = "Pivot To Cursor" - bl_description = "Pivot Point To Cursor" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.origin_set(type='ORIGIN_CURSOR') - bpy.ops.object.mode_set(mode='EDIT') - - return {'FINISHED'} - - -# Origin to Center of Mass Edit Mode -class PIE_OT_OriginToMass_edit(Operator): - bl_idname = "object.origintomass_edit" - bl_label = "Origin" - bl_description = "Origin to Center of Mass" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') - bpy.ops.object.mode_set(mode='EDIT') - - return {'FINISHED'} - - -# Origin to Geometry Edit Mode -class PIE_OT_OriginToGeometry_edit(Operator): - bl_idname = "object.origintogeometry_edit" - bl_label = "Origin to Geometry" - bl_description = "Origin to Geometry" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN') - bpy.ops.object.mode_set(mode='EDIT') - - return {'FINISHED'} - - -# Origin to Geometry Edit Mode -class PIE_OT_GeometryToOrigin_edit(Operator): - bl_idname = "object.geometrytoorigin_edit" - bl_label = "Geometry to Origin" - bl_description = "Geometry to Origin" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='MEDIAN') - bpy.ops.object.mode_set(mode='EDIT') - - return {'FINISHED'} - - -# Origin To Selected Edit Mode # -def vfeOrigin_pie(context): - try: - cursorPositionX = context.scene.cursor.location[0] - cursorPositionY = context.scene.cursor.location[1] - cursorPositionZ = context.scene.cursor.location[2] - bpy.ops.view3d.snap_cursor_to_selected() - bpy.ops.object.mode_set() - bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') - bpy.ops.object.mode_set(mode='EDIT') - context.scene.cursor.location[0] = cursorPositionX - context.scene.cursor.location[1] = cursorPositionY - context.scene.cursor.location[2] = cursorPositionZ + if not any([ob.type in {'MESH', 'ARMATURE'} for ob in context.selected_objects]): + cls.poll_message_set("No mesh or armature objects selected.") + return False return True - except: - return False - - -class PIE_OT_SetOriginToSelected_edit(Operator): - bl_idname = "object.setorigintoselected_edit" - bl_label = "Set Origin to Selected" - bl_description = "Set Origin to Selected" - - @classmethod - def poll(cls, context): - return context.area.type == "VIEW_3D" and context.active_object is not None def execute(self, context): - check = vfeOrigin_pie(context) - if not check: - self.report({"ERROR"}, "Set Origin to Selected could not be performed") - return {'CANCELLED'} + org_active_obj = context.active_object + + counter = 0 + for obj in context.selected_objects: + counter += int(self.origin_to_bottom(context, obj)) + + context.view_layer.objects.active = org_active_obj + self.report({'INFO'}, f"Moved the origins of {counter} objects to their lowest point.") return {'FINISHED'} + @staticmethod + def origin_to_bottom(context, obj) -> bool: + if obj.type not in {'MESH', 'ARMATURE'}: + return False + + org_mode = obj.mode + + if obj.type == 'MESH': + bpy.ops.object.mode_set(mode='OBJECT') + min_z = min([v.co.z for v in obj.data.vertices]) + elif obj.type == 'ARMATURE': + context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT') + min_z = min([min([bone.head.z, bone.tail.z]) for bone in obj.data.edit_bones]) + else: + return False + + if obj.type == 'MESH': + for vert in obj.data.vertices: + vert.co.z -= min_z + elif obj.type == 'ARMATURE': + for bone in obj.data.edit_bones: + bone.head.z -= min_z + bone.tail.z -= min_z + + obj.location.z += min_z + + bpy.ops.object.mode_set(mode=org_mode) + return True + + +class PIE_OT_set_origin_any_mode(Operator): + bl_idname = "object.origin_set_any_mode" + bl_label = "Set Origin" + bl_description = "Set the Object Origin" + bl_options = {'REGISTER', 'UNDO'} + + type: StringProperty() + + messages = { + 'GEOMETRY_ORIGIN' : "Snapped the geometry to the object's origin.", + 'ORIGIN_CURSOR' : "Snapped the object's origin to the 3D cursor.", + 'ORIGIN_CENTER_OF_MASS': "Snapped the origin to the geometry's center of mass.", + 'ORIGIN_CENTER_OF_VOLUME': "Snapped the origin to the geometry's center of volume.", + 'ORIGIN_GEOMETRY': "Snapped the origin to the geometry.", + } + + @classmethod + def description(cls, context, params): + return cls.messages[params.type].replace("Snapped", "Snap")[:-1] + + def execute(self, context): + org_mode = 'OBJECT' + if context.active_object: + org_mode = context.active_object.mode + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.origin_set(type=self.type) + bpy.ops.object.mode_set(mode=org_mode) + + self.report({'INFO'}, self.messages[self.type]) + + return {'FINISHED'} + -# Pie Origin/Pivot - Shift + S class PIE_MT_OriginPivot(Menu): bl_idname = "ORIGIN_MT_pivotmenu" bl_label = "Origin Pie" def draw(self, context): - layout = self.layout - obj = context.object - pie = layout.menu_pie() - if obj and obj.type == 'MESH' and obj.mode in {'OBJECT'}: - # 4 - LEFT - pie.operator( - "object.origin_set", text="Origin to Center of Mass", icon='NONE' - ).type = 'ORIGIN_CENTER_OF_MASS' - # 6 - RIGHT - pie.operator( - "object.origin_set", text="Origin to Cursor", icon='PIVOT_CURSOR' - ).type = 'ORIGIN_CURSOR' - # 2 - BOTTOM - pie.operator( - "object.pivotobottom", text="Origin to Bottom", icon='TRIA_DOWN' - ) - # 8 - TOP - pie.operator( - "object.pivot2selection", - text="Origin To Selection", - icon='SNAP_INCREMENT', - ) - # 7 - TOP - LEFT - pie.operator( - "object.origin_set", text="Geometry To Origin", icon='NONE' - ).type = 'GEOMETRY_ORIGIN' - # 9 - TOP - RIGHT - pie.operator( - "object.origin_set", text="Origin To Geometry", icon='NONE' - ).type = 'ORIGIN_GEOMETRY' + pie = self.layout.menu_pie() - elif obj and obj.type == 'MESH' and obj.mode in {'EDIT'}: - # 4 - LEFT - pie.operator( - "object.origintomass_edit", text="Origin to Center of Mass", icon='NONE' - ) - # 6 - RIGHT - pie.operator( - "object.pivot2cursor_edit", text="Origin to Cursor", icon='PIVOT_CURSOR' - ) - # 2 - BOTTOM - pie.operator( - "object.pivotobottom_edit", text="Origin to Bottom", icon='TRIA_DOWN' - ) - # 8 - TOP - pie.operator( - "object.setorigintoselected_edit", - text="Origin To Selected", - icon='SNAP_INCREMENT', - ) - # 7 - TOP - LEFT - pie.operator( - "object.geometrytoorigin_edit", text="Geometry To Origin", icon='NONE' - ) - # 9 - TOP - RIGHT - pie.operator( - "object.origintogeometry_edit", text="Origin To Geometry", icon='NONE' - ) - - else: - # 4 - LEFT - pie.operator( - "object.origin_set", text="Origin to Center of Mass", icon='NONE' - ).type = 'ORIGIN_CENTER_OF_MASS' - # 6 - RIGHT - pie.operator( - "object.origin_set", text="Origin To 3D Cursor", icon='PIVOT_CURSOR' - ).type = 'ORIGIN_CURSOR' - # 2 - BOTTOM - pie.operator( - "object.pivot2selection", - text="Origin To Selection", - icon='SNAP_INCREMENT', - ) - # 8 - TOP - pie.operator( - "object.origin_set", text="Origin To Geometry", icon='NONE' - ).type = 'ORIGIN_GEOMETRY' - # 7 - TOP - LEFT - pie.operator( - "object.origin_set", text="Geometry To Origin", icon='NONE' - ).type = 'GEOMETRY_ORIGIN' + # 4 - LEFT + pie.operator( + "object.origin_set_any_mode", text="Geometry -> Origin", icon='TRANSFORM_ORIGINS' + ).type = 'GEOMETRY_ORIGIN' + # 6 - RIGHT + pie.operator( + "object.origin_set_any_mode", text="Origin -> Geometry", icon='SNAP_PEEL_OBJECT' + ).type = 'ORIGIN_GEOMETRY' + # 2 - BOTTOM + pie.operator( + "object.origin_set_to_bottom", text="Origin -> Bottom", icon='TRIA_DOWN' + ) + # 8 - TOP + pie.operator( + "object.origin_set_to_selection", + text="Origin -> Selection", + icon='RESTRICT_SELECT_OFF', + ) + # 7 - TOP - LEFT + pie.operator( + "object.origin_set_any_mode", text="Origin -> Cursor", icon='PIVOT_CURSOR' + ).type = 'ORIGIN_CURSOR' + # 9 - TOP - RIGHT + pie.operator( + "object.origin_set_any_mode", text="Origin -> Mass", icon='SNAP_FACE_CENTER' + ).type = 'ORIGIN_CENTER_OF_MASS' + # 1 - BOTTOM - LEFT + pie.separator() + # 3 - BOTTOM - RIGHT + pie.operator( + "object.origin_set_any_mode", text="Origin -> Volume", icon='SNAP_FACE_CENTER' + ).type = 'ORIGIN_CENTER_OF_MASS' registry = ( PIE_MT_OriginPivot, - PIE_OT_PivotToSelection, - PIE_OT_PivotBottom, - PIE_OT_PivotToCursor_edit, - PIE_OT_OriginToMass_edit, - PIE_OT_PivotBottom_edit, - PIE_OT_OriginToGeometry_edit, - PIE_OT_GeometryToOrigin_edit, - PIE_OT_SetOriginToSelected_edit, + PIE_OT_set_origin_to_selection, + PIE_OT_set_origin_to_bottom, + PIE_OT_set_origin_any_mode, ) -- 2.30.2 From 8209bdc19bbe89c09eee0230a27ea311545a1706 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Sun, 18 Aug 2024 11:19:39 +0100 Subject: [PATCH 2/2] Fix potential error in origins_to_bottom with empty objects --- source/pie_origin.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/source/pie_origin.py b/source/pie_origin.py index 02ff85e..39836bd 100644 --- a/source/pie_origin.py +++ b/source/pie_origin.py @@ -63,14 +63,18 @@ class PIE_OT_set_origin_to_bottom(Operator): org_mode = obj.mode - if obj.type == 'MESH': - bpy.ops.object.mode_set(mode='OBJECT') - min_z = min([v.co.z for v in obj.data.vertices]) - elif obj.type == 'ARMATURE': - context.view_layer.objects.active = obj - bpy.ops.object.mode_set(mode='EDIT') - min_z = min([min([bone.head.z, bone.tail.z]) for bone in obj.data.edit_bones]) - else: + try: + if obj.type == 'MESH': + bpy.ops.object.mode_set(mode='OBJECT') + min_z = min([v.co.z for v in obj.data.vertices]) + elif obj.type == 'ARMATURE': + context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT') + min_z = min([min([bone.head.z, bone.tail.z]) for bone in obj.data.edit_bones]) + else: + return False + except ValueError: + # min([]) would result in this error, so if the object is empty. return False if obj.type == 'MESH': -- 2.30.2