bl_info = { "name": "Drop to Floor / Plane", "author": "D3rp1na", "version": (0, 1), "blender": (3, 0, 0), "location": "View3D > Object > Drop to Floor (also in Object Context Menu)", "description": "Drops selected objects down onto Z=0, a custom height, or the nearest surface below via raycast", "category": "Object", } import bpy import bmesh from mathutils import Vector def get_world_bbox_corners(obj): """Return the 8 bounding box corners of obj in world space.""" return [obj.matrix_world @ Vector(c) for c in obj.bound_box] def lowest_world_z(obj): """Lowest Z value of the object's bounding box in world space.""" return min(c.z for c in get_world_bbox_corners(obj)) def lowest_point_xy(obj): """The (x, y) location of the bounding-box corner with the lowest Z, used as the raycast origin so we hit whatever is directly under the object.""" corners = get_world_bbox_corners(obj) lowest = min(corners, key=lambda c: c.z) return lowest.x, lowest.y class OBJECT_OT_drop_to_floor(bpy.types.Operator): """Drop selected objects down to Z=0, a custom height, or the surface below them""" bl_idname = "object.drop_to_floor" bl_label = "Drop to Floor" bl_options = {'REGISTER', 'UNDO'} mode: bpy.props.EnumProperty( name="Mode", items=[ ('ZERO', "Floor (Z = 0)", "Drop objects so their lowest point touches Z=0"), ('CUSTOM', "Custom Height", "Drop objects so their lowest point touches a chosen Z height"), ('RAYCAST', "Raycast (nearest surface)", "Cast a ray straight down and land on whatever geometry is hit; falls back to the custom height if nothing is hit"), ], default='ZERO', ) custom_z: bpy.props.FloatProperty( name="Target Z", description="Z height to drop onto (used for Custom mode, and as the raycast fallback)", default=0.0, ) use_bbox_center_xy: bpy.props.BoolProperty( name="Raycast from bbox center (XY)", description="If enabled, raycast straight down from the object's horizontal bbox center instead of from directly under its lowest point", default=False, ) def execute(self, context): selected = [o for o in context.selected_objects if o.type in {'MESH', 'CURVE', 'EMPTY', 'SURFACE', 'FONT'}] if not selected: self.report({'WARNING'}, "No valid objects selected") return {'CANCELLED'} depsgraph = context.evaluated_depsgraph_get() for obj in selected: current_low = lowest_world_z(obj) target_z = self.custom_z if self.mode == 'ZERO': target_z = 0.0 elif self.mode == 'CUSTOM': target_z = self.custom_z elif self.mode == 'RAYCAST': if self.use_bbox_center_xy: # average of all 8 corners' x/y corners = get_world_bbox_corners(obj) x = sum(c.x for c in corners) / 8.0 y = sum(c.y for c in corners) / 8.0 else: x, y = lowest_point_xy(obj) ray_origin = Vector((x, y, current_low + 10000.0)) ray_direction = Vector((0, 0, -1)) result, location, normal, index, hit_obj, matrix = context.scene.ray_cast( depsgraph, ray_origin, ray_direction ) if result and hit_obj != obj: target_z = location.z else: target_z = self.custom_z # fallback if nothing hit delta_z = target_z - current_low obj.location.z += delta_z return {'FINISHED'} def menu_func(self, context): self.layout.operator(OBJECT_OT_drop_to_floor.bl_idname, icon='TRIA_DOWN_BAR') def register(): bpy.utils.register_class(OBJECT_OT_drop_to_floor) bpy.types.VIEW3D_MT_object.append(menu_func) bpy.types.VIEW3D_MT_object_context_menu.append(menu_func) def unregister(): bpy.types.VIEW3D_MT_object.remove(menu_func) bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func) bpy.utils.unregister_class(OBJECT_OT_drop_to_floor) if __name__ == "__main__": register()