Utility_Apps/Blender/simple scripts/move objects down to the top of a surface/move objects down to the top of a surface.py

77 lines
2.4 KiB
Python

import bpy
import bmesh
from mathutils import Vector
from mathutils.bvhtree import BVHTree
DEBUG_LEVEL = 2 # 0=minimal, 1=per-object, 2=verbose
def dbg(level, *args):
if DEBUG_LEVEL >= level:
print(*args)
def get_world_bvhtree(obj):
if obj.type != 'MESH':
return None
depsgraph = bpy.context.evaluated_depsgraph_get()
eval_obj = obj.evaluated_get(depsgraph)
mesh = eval_obj.to_mesh()
bm = bmesh.new()
bm.from_mesh(mesh)
bm.transform(obj.matrix_world)
bvh = BVHTree.FromBMesh(bm)
bm.free()
eval_obj.to_mesh_clear()
return bvh
def snap_objects_down_z():
active_obj = bpy.context.view_layer.objects.active
selected_objs = [o for o in bpy.context.selected_objects if o != active_obj]
if not active_obj or active_obj.type != 'MESH':
print("ERROR: Active object must be a mesh.")
return
bvh = get_world_bvhtree(active_obj)
if not bvh:
print("ERROR: Could not build BVH for active object.")
return
dbg(1, f"Active object: {active_obj.name}")
dbg(1, f"Selected objects: {[o.name for o in selected_objs]}")
for obj in selected_objs:
if obj.type != 'MESH':
dbg(1, f"Skipping '{obj.name}': not a mesh")
continue
depsgraph = bpy.context.evaluated_depsgraph_get()
eval_obj = obj.evaluated_get(depsgraph)
mesh = eval_obj.to_mesh()
if not mesh.vertices:
eval_obj.to_mesh_clear()
dbg(1, f"Skipping '{obj.name}': no vertices")
continue
# Find lowest vertex in world coordinates
world_verts = [obj.matrix_world @ v.co for v in mesh.vertices]
lowest_vert = min(world_verts, key=lambda v: v.z)
dbg(2, f"'{obj.name}' lowest vertex: {lowest_vert}")
# Cast a ray straight down from the lowest vertex
ray_origin = lowest_vert + Vector((0,0,0.01)) # tiny offset to avoid self-intersection
ray_dir = Vector((0,0,-1))
hit_loc, hit_normal, hit_index, hit_dist = bvh.ray_cast(ray_origin, ray_dir, 10000.0)
if hit_loc:
dbg(2, f"'{obj.name}' hit at {hit_loc}")
z_offset = hit_loc.z - lowest_vert.z
dbg(1, f"'{obj.name}' moving down by {z_offset:.6f} in Z")
obj.location.z += z_offset
else:
dbg(1, f"'{obj.name}' did not hit active object when casting down")
eval_obj.to_mesh_clear()
print("Snap down complete.")
snap_objects_down_z()