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.
113 lines
3.6 KiB
Python
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()
|