Utility_Apps/Blender/addons/Object Transform Tools/__init__.py
Ryan Schultz b907d848f1 Add Snap to Surface operator; rename plugin to Object Transform Tools
Incorporates "move objects down to the top of a surface" as a proper
OBJECT_OT_snap_to_surface operator using BVH raycasting to drop selected
objects flush onto the active mesh. Adds a menu separator to group the
two new entries from Blender's built-in transform items.
2026-04-19 08:14:27 -05:00

113 lines
3.6 KiB
Python

bl_info = {
"name": "Object Transform Tools",
"author": "Ryan Schultz",
"version": (1, 1),
"blender": (4, 0, 0),
"location": "3D View > Object > Transform",
"description": "Match active object transform; snap selected objects down onto a surface",
"category": "Object",
}
import bpy
import bmesh
from mathutils import Vector
from mathutils.bvhtree import BVHTree
class OBJECT_OT_match_active(bpy.types.Operator):
"""Match location and rotation of selected objects to the active object"""
bl_idname = "object.match_active_transform"
bl_label = "Match Active Object"
bl_options = {'REGISTER', 'UNDO'}
match_scale: bpy.props.BoolProperty(
name="Match Scale",
description="Also match the scale of the active object",
default=False,
)
def execute(self, context):
target = context.active_object
if not target:
self.report({'WARNING'}, "No active object selected")
return {'CANCELLED'}
for obj in context.selected_objects:
if obj != target:
obj.location = target.location.copy()
obj.rotation_euler = target.rotation_euler.copy()
if self.match_scale:
obj.scale = target.scale.copy()
return {'FINISHED'}
class OBJECT_OT_snap_to_surface(bpy.types.Operator):
"""Snap selected objects down in Z so their lowest vertex rests on the active mesh surface"""
bl_idname = "object.snap_to_surface"
bl_label = "Snap to Surface"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
active_obj = context.active_object
selected_objs = [o for o in context.selected_objects if o != active_obj]
if not active_obj or active_obj.type != 'MESH':
self.report({'WARNING'}, "Active object must be a mesh")
return {'CANCELLED'}
depsgraph = context.evaluated_depsgraph_get()
eval_active = active_obj.evaluated_get(depsgraph)
mesh = eval_active.to_mesh()
bm = bmesh.new()
bm.from_mesh(mesh)
bm.transform(active_obj.matrix_world)
bvh = BVHTree.FromBMesh(bm)
bm.free()
eval_active.to_mesh_clear()
moved = 0
for obj in selected_objs:
if obj.type != 'MESH':
continue
eval_obj = obj.evaluated_get(depsgraph)
obj_mesh = eval_obj.to_mesh()
if not obj_mesh.vertices:
eval_obj.to_mesh_clear()
continue
world_verts = [obj.matrix_world @ v.co for v in obj_mesh.vertices]
lowest = min(world_verts, key=lambda v: v.z)
eval_obj.to_mesh_clear()
ray_origin = lowest + Vector((0, 0, 0.01))
hit_loc, _, _, _ = bvh.ray_cast(ray_origin, Vector((0, 0, -1)), 10000.0)
if hit_loc:
obj.location.z += hit_loc.z - lowest.z
moved += 1
self.report({'INFO'}, f"Snapped {moved} object(s) to surface")
return {'FINISHED'}
def menu_func(self, context):
self.layout.separator()
self.layout.operator(OBJECT_OT_match_active.bl_idname)
self.layout.operator(OBJECT_OT_snap_to_surface.bl_idname)
def register():
bpy.utils.register_class(OBJECT_OT_match_active)
bpy.utils.register_class(OBJECT_OT_snap_to_surface)
bpy.types.VIEW3D_MT_transform_object.append(menu_func)
def unregister():
bpy.types.VIEW3D_MT_transform_object.remove(menu_func)
bpy.utils.unregister_class(OBJECT_OT_snap_to_surface)
bpy.utils.unregister_class(OBJECT_OT_match_active)
if __name__ == "__main__":
register()