object_align.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. # ##### BEGIN GPL LICENSE BLOCK #####
  2. #
  3. # This program is free software; you can redistribute it and/or
  4. # modify it under the terms of the GNU General Public License
  5. # as published by the Free Software Foundation; either version 2
  6. # of the License, or (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software Foundation,
  15. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. #
  17. # ##### END GPL LICENSE BLOCK #####
  18. # <pep8-80 compliant>
  19. import bpy
  20. from bpy.types import Operator
  21. from mathutils import Vector
  22. def worldspace_bounds_from_object_bounds(bb_world):
  23. # Initialize the variables with the 8th vertex
  24. left, right, front, back, down, up = (
  25. bb_world[7][0],
  26. bb_world[7][0],
  27. bb_world[7][1],
  28. bb_world[7][1],
  29. bb_world[7][2],
  30. bb_world[7][2],
  31. )
  32. # Test against the other 7 verts
  33. for i in range(7):
  34. # X Range
  35. val = bb_world[i][0]
  36. if val < left:
  37. left = val
  38. if val > right:
  39. right = val
  40. # Y Range
  41. val = bb_world[i][1]
  42. if val < front:
  43. front = val
  44. if val > back:
  45. back = val
  46. # Z Range
  47. val = bb_world[i][2]
  48. if val < down:
  49. down = val
  50. if val > up:
  51. up = val
  52. return (Vector((left, front, up)), Vector((right, back, down)))
  53. def worldspace_bounds_from_object_data(depsgraph, obj):
  54. matrix_world = obj.matrix_world.copy()
  55. # Initialize the variables with the last vertex
  56. ob_eval = obj.evaluated_get(depsgraph)
  57. me = ob_eval.to_mesh()
  58. verts = me.vertices
  59. val = matrix_world @ (verts[-1].co if verts else Vector((0.0, 0.0, 0.0)))
  60. left, right, front, back, down, up = (
  61. val[0],
  62. val[0],
  63. val[1],
  64. val[1],
  65. val[2],
  66. val[2],
  67. )
  68. # Test against all other verts
  69. for v in verts:
  70. vco = matrix_world @ v.co
  71. # X Range
  72. val = vco[0]
  73. if val < left:
  74. left = val
  75. if val > right:
  76. right = val
  77. # Y Range
  78. val = vco[1]
  79. if val < front:
  80. front = val
  81. if val > back:
  82. back = val
  83. # Z Range
  84. val = vco[2]
  85. if val < down:
  86. down = val
  87. if val > up:
  88. up = val
  89. ob_eval.to_mesh_clear()
  90. return Vector((left, front, up)), Vector((right, back, down))
  91. def align_objects(context,
  92. align_x,
  93. align_y,
  94. align_z,
  95. align_mode,
  96. relative_to,
  97. bb_quality):
  98. depsgraph = context.evaluated_depsgraph_get()
  99. scene = context.scene
  100. cursor = scene.cursor.location
  101. # We are accessing runtime data such as evaluated bounding box, so we need to
  102. # be sure it is properly updated and valid (bounding box might be lost on operator
  103. # redo).
  104. context.view_layer.update()
  105. Left_Front_Up_SEL = [0.0, 0.0, 0.0]
  106. Right_Back_Down_SEL = [0.0, 0.0, 0.0]
  107. flag_first = True
  108. objects = []
  109. for obj in context.selected_objects:
  110. matrix_world = obj.matrix_world.copy()
  111. bb_world = [matrix_world @ Vector(v) for v in obj.bound_box]
  112. objects.append((obj, bb_world))
  113. if not objects:
  114. return False
  115. for obj, bb_world in objects:
  116. if bb_quality and obj.type == 'MESH':
  117. GBB = worldspace_bounds_from_object_data(depsgraph, obj)
  118. else:
  119. GBB = worldspace_bounds_from_object_bounds(bb_world)
  120. Left_Front_Up = GBB[0]
  121. Right_Back_Down = GBB[1]
  122. # Active Center
  123. if obj == context.active_object:
  124. center_active_x = (Left_Front_Up[0] + Right_Back_Down[0]) / 2.0
  125. center_active_y = (Left_Front_Up[1] + Right_Back_Down[1]) / 2.0
  126. center_active_z = (Left_Front_Up[2] + Right_Back_Down[2]) / 2.0
  127. size_active_x = (Right_Back_Down[0] - Left_Front_Up[0]) / 2.0
  128. size_active_y = (Right_Back_Down[1] - Left_Front_Up[1]) / 2.0
  129. size_active_z = (Left_Front_Up[2] - Right_Back_Down[2]) / 2.0
  130. # Selection Center
  131. if flag_first:
  132. flag_first = False
  133. Left_Front_Up_SEL[0] = Left_Front_Up[0]
  134. Left_Front_Up_SEL[1] = Left_Front_Up[1]
  135. Left_Front_Up_SEL[2] = Left_Front_Up[2]
  136. Right_Back_Down_SEL[0] = Right_Back_Down[0]
  137. Right_Back_Down_SEL[1] = Right_Back_Down[1]
  138. Right_Back_Down_SEL[2] = Right_Back_Down[2]
  139. else:
  140. # X axis
  141. if Left_Front_Up[0] < Left_Front_Up_SEL[0]:
  142. Left_Front_Up_SEL[0] = Left_Front_Up[0]
  143. # Y axis
  144. if Left_Front_Up[1] < Left_Front_Up_SEL[1]:
  145. Left_Front_Up_SEL[1] = Left_Front_Up[1]
  146. # Z axis
  147. if Left_Front_Up[2] > Left_Front_Up_SEL[2]:
  148. Left_Front_Up_SEL[2] = Left_Front_Up[2]
  149. # X axis
  150. if Right_Back_Down[0] > Right_Back_Down_SEL[0]:
  151. Right_Back_Down_SEL[0] = Right_Back_Down[0]
  152. # Y axis
  153. if Right_Back_Down[1] > Right_Back_Down_SEL[1]:
  154. Right_Back_Down_SEL[1] = Right_Back_Down[1]
  155. # Z axis
  156. if Right_Back_Down[2] < Right_Back_Down_SEL[2]:
  157. Right_Back_Down_SEL[2] = Right_Back_Down[2]
  158. center_sel_x = (Left_Front_Up_SEL[0] + Right_Back_Down_SEL[0]) / 2.0
  159. center_sel_y = (Left_Front_Up_SEL[1] + Right_Back_Down_SEL[1]) / 2.0
  160. center_sel_z = (Left_Front_Up_SEL[2] + Right_Back_Down_SEL[2]) / 2.0
  161. # Main Loop
  162. for obj, bb_world in objects:
  163. matrix_world = obj.matrix_world.copy()
  164. bb_world = [matrix_world @ Vector(v[:]) for v in obj.bound_box]
  165. if bb_quality and obj.type == 'MESH':
  166. GBB = worldspace_bounds_from_object_data(depsgraph, obj)
  167. else:
  168. GBB = worldspace_bounds_from_object_bounds(bb_world)
  169. Left_Front_Up = GBB[0]
  170. Right_Back_Down = GBB[1]
  171. center_x = (Left_Front_Up[0] + Right_Back_Down[0]) / 2.0
  172. center_y = (Left_Front_Up[1] + Right_Back_Down[1]) / 2.0
  173. center_z = (Left_Front_Up[2] + Right_Back_Down[2]) / 2.0
  174. positive_x = Right_Back_Down[0]
  175. positive_y = Right_Back_Down[1]
  176. positive_z = Left_Front_Up[2]
  177. negative_x = Left_Front_Up[0]
  178. negative_y = Left_Front_Up[1]
  179. negative_z = Right_Back_Down[2]
  180. obj_loc = obj.location
  181. if align_x:
  182. # Align Mode
  183. if relative_to == 'OPT_4': # Active relative
  184. if align_mode == 'OPT_1':
  185. obj_x = obj_loc[0] - negative_x - size_active_x
  186. elif align_mode == 'OPT_3':
  187. obj_x = obj_loc[0] - positive_x + size_active_x
  188. else: # Everything else relative
  189. if align_mode == 'OPT_1':
  190. obj_x = obj_loc[0] - negative_x
  191. elif align_mode == 'OPT_3':
  192. obj_x = obj_loc[0] - positive_x
  193. if align_mode == 'OPT_2': # All relative
  194. obj_x = obj_loc[0] - center_x
  195. # Relative To
  196. if relative_to == 'OPT_1':
  197. loc_x = obj_x
  198. elif relative_to == 'OPT_2':
  199. loc_x = obj_x + cursor[0]
  200. elif relative_to == 'OPT_3':
  201. loc_x = obj_x + center_sel_x
  202. elif relative_to == 'OPT_4':
  203. loc_x = obj_x + center_active_x
  204. obj.location[0] = loc_x
  205. if align_y:
  206. # Align Mode
  207. if relative_to == 'OPT_4': # Active relative
  208. if align_mode == 'OPT_1':
  209. obj_y = obj_loc[1] - negative_y - size_active_y
  210. elif align_mode == 'OPT_3':
  211. obj_y = obj_loc[1] - positive_y + size_active_y
  212. else: # Everything else relative
  213. if align_mode == 'OPT_1':
  214. obj_y = obj_loc[1] - negative_y
  215. elif align_mode == 'OPT_3':
  216. obj_y = obj_loc[1] - positive_y
  217. if align_mode == 'OPT_2': # All relative
  218. obj_y = obj_loc[1] - center_y
  219. # Relative To
  220. if relative_to == 'OPT_1':
  221. loc_y = obj_y
  222. elif relative_to == 'OPT_2':
  223. loc_y = obj_y + cursor[1]
  224. elif relative_to == 'OPT_3':
  225. loc_y = obj_y + center_sel_y
  226. elif relative_to == 'OPT_4':
  227. loc_y = obj_y + center_active_y
  228. obj.location[1] = loc_y
  229. if align_z:
  230. # Align Mode
  231. if relative_to == 'OPT_4': # Active relative
  232. if align_mode == 'OPT_1':
  233. obj_z = obj_loc[2] - negative_z - size_active_z
  234. elif align_mode == 'OPT_3':
  235. obj_z = obj_loc[2] - positive_z + size_active_z
  236. else: # Everything else relative
  237. if align_mode == 'OPT_1':
  238. obj_z = obj_loc[2] - negative_z
  239. elif align_mode == 'OPT_3':
  240. obj_z = obj_loc[2] - positive_z
  241. if align_mode == 'OPT_2': # All relative
  242. obj_z = obj_loc[2] - center_z
  243. # Relative To
  244. if relative_to == 'OPT_1':
  245. loc_z = obj_z
  246. elif relative_to == 'OPT_2':
  247. loc_z = obj_z + cursor[2]
  248. elif relative_to == 'OPT_3':
  249. loc_z = obj_z + center_sel_z
  250. elif relative_to == 'OPT_4':
  251. loc_z = obj_z + center_active_z
  252. obj.location[2] = loc_z
  253. return True
  254. from bpy.props import (
  255. BoolProperty,
  256. EnumProperty,
  257. )
  258. class AlignObjects(Operator):
  259. """Align Objects"""
  260. bl_idname = "object.align"
  261. bl_label = "Align Objects"
  262. bl_options = {'REGISTER', 'UNDO'}
  263. bb_quality: BoolProperty(
  264. name="High Quality",
  265. description=(
  266. "Enables high quality calculation of the "
  267. "bounding box for perfect results on complex "
  268. "shape meshes with rotation/scale (Slow)"
  269. ),
  270. default=True,
  271. )
  272. align_mode: EnumProperty(
  273. name="Align Mode:",
  274. description="Side of object to use for alignment",
  275. items=(
  276. ('OPT_1', "Negative Sides", ""),
  277. ('OPT_2', "Centers", ""),
  278. ('OPT_3', "Positive Sides", ""),
  279. ),
  280. default='OPT_2',
  281. )
  282. relative_to: EnumProperty(
  283. name="Relative To:",
  284. description="Reference location to align to",
  285. items=(
  286. ('OPT_1', "Scene Origin", "Use the Scene Origin as the position for the selected objects to align to"),
  287. ('OPT_2', "3D Cursor", "Use the 3D cursor as the position for the selected objects to align to"),
  288. ('OPT_3', "Selection", "Use the selected objects as the position for the selected objects to align to"),
  289. ('OPT_4', "Active", "Use the active object as the position for the selected objects to align to"),
  290. ),
  291. default='OPT_4',
  292. )
  293. align_axis: EnumProperty(
  294. name="Align",
  295. description="Align to axis",
  296. items=(
  297. ('X', "X", ""),
  298. ('Y', "Y", ""),
  299. ('Z', "Z", ""),
  300. ),
  301. options={'ENUM_FLAG'},
  302. )
  303. @classmethod
  304. def poll(cls, context):
  305. return context.mode == 'OBJECT'
  306. def execute(self, context):
  307. align_axis = self.align_axis
  308. ret = align_objects(
  309. context,
  310. 'X' in align_axis,
  311. 'Y' in align_axis,
  312. 'Z' in align_axis,
  313. self.align_mode,
  314. self.relative_to,
  315. self.bb_quality,
  316. )
  317. if not ret:
  318. self.report({'WARNING'}, "No objects with bound-box selected")
  319. return {'CANCELLED'}
  320. else:
  321. return {'FINISHED'}
  322. classes = (
  323. AlignObjects,
  324. )