-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdrop_to_surface.py
More file actions
141 lines (107 loc) · 4.84 KB
/
drop_to_surface.py
File metadata and controls
141 lines (107 loc) · 4.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""
Drop to Surface - Projects selected objects onto the surface beneath them.
Uses raycasting to find the surface in a chosen direction and moves each
object so its leading edge (bounding box face in the drop direction) lands
exactly on that surface. Optionally aligns rotation to the surface normal.
"""
import bpy
import mathutils
from typing import Optional
# World-space ray directions for each axis option
AXIS_DIRECTION = {
'NEG_Z': mathutils.Vector(( 0, 0, -1)),
'POS_Z': mathutils.Vector(( 0, 0, 1)),
'NEG_X': mathutils.Vector((-1, 0, 0)),
'POS_X': mathutils.Vector(( 1, 0, 0)),
'NEG_Y': mathutils.Vector(( 0, -1, 0)),
'POS_Y': mathutils.Vector(( 0, 1, 0)),
}
# Object types that have a meaningful bounding box
BBOX_TYPES = {
'MESH', 'CURVE', 'SURFACE', 'META', 'FONT', 'ARMATURE', 'LATTICE',
}
def drop_to_surface(context, axis: str, align_to_normal: bool) -> Optional[str]:
"""Project each selected object onto the nearest surface along the given axis.
For each object the function:
1. Computes the bounding-box extent (leading edge) in the drop direction.
2. Casts a ray starting just past that edge to avoid self-intersection.
3. Slides the object along the axis so its leading edge rests on the hit.
4. Optionally rotates the object to match the surface normal.
Args:
context: Current Blender context.
axis: Drop direction identifier (e.g. 'NEG_Z').
align_to_normal: Rotate objects to match the hit surface normal.
Returns:
Error string if nothing could be dropped, None on success.
"""
selected = list(context.selected_objects)
if not selected:
return "No objects selected"
ray_dir = AXIS_DIRECTION[axis]
depsgraph = context.evaluated_depsgraph_get()
dropped = 0
for obj in selected:
if obj.type in {'LIGHT', 'CAMERA', 'SPEAKER', 'LIGHT_PROBE'}:
continue
origin_dist = obj.matrix_world.translation.dot(ray_dir)
# Leading edge: the bounding-box corner furthest in the drop direction
if obj.type in BBOX_TYPES:
eval_obj = obj.evaluated_get(depsgraph)
world_bbox = [obj.matrix_world @ mathutils.Vector(c)
for c in eval_obj.bound_box]
leading_dist = max(c.dot(ray_dir) for c in world_bbox)
else:
leading_dist = origin_dist # Empties: leading edge = origin
delta = leading_dist - origin_dist # offset: origin → leading edge
# Start ray just beyond the leading edge → no self-intersection
ray_origin = obj.matrix_world.translation + ray_dir * (delta + 0.001)
hit, hit_loc, hit_normal, _idx, _hit_obj, _mat = context.scene.ray_cast(
depsgraph, ray_origin, ray_dir
)
if not hit:
continue # Nothing found in this direction for this object
# Move object so leading edge touches hit surface
target_origin_dist = hit_loc.dot(ray_dir) - delta
displacement = target_origin_dist - origin_dist
obj.location = obj.location + ray_dir * displacement
if align_to_normal:
rot_quat = hit_normal.to_track_quat('Z', 'Y')
obj.rotation_euler = rot_quat.to_euler()
dropped += 1
if dropped == 0:
return (
"No objects dropped — no surface found in the selected direction. "
"Make sure there is geometry below the selected objects."
)
return None
class OBJECT_OT_drop_to_surface(bpy.types.Operator):
"""Project selected objects onto the nearest surface in the chosen direction"""
bl_idname = "object.drop_to_surface"
bl_label = "Drop to Surface"
bl_description = (
"Cast a ray from each selected object and land it on the nearest surface "
"in the chosen direction. Optionally rotates each object to match the "
"surface normal. Useful for placing props on terrain or floors"
)
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'OBJECT' and len(context.selected_objects) > 0
def execute(self, context):
wm = context.window_manager
props = wm.pivotier.drop_to_surface
error = drop_to_surface(context, props.axis, props.align_to_normal)
if error:
self.report({'WARNING'}, error)
return {'CANCELLED'}
return {'FINISHED'}
# ─── Registration ─────────────────────────────────────────────────────────────
classes = (
OBJECT_OT_drop_to_surface,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)