object.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  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 bpy.props import (
  22. BoolProperty,
  23. EnumProperty,
  24. IntProperty,
  25. StringProperty,
  26. )
  27. class SelectPattern(Operator):
  28. """Select objects matching a naming pattern"""
  29. bl_idname = "object.select_pattern"
  30. bl_label = "Select Pattern"
  31. bl_options = {'REGISTER', 'UNDO'}
  32. pattern: StringProperty(
  33. name="Pattern",
  34. description="Name filter using '*', '?' and "
  35. "'[abc]' unix style wildcards",
  36. maxlen=64,
  37. default="*",
  38. )
  39. case_sensitive: BoolProperty(
  40. name="Case Sensitive",
  41. description="Do a case sensitive compare",
  42. default=False,
  43. )
  44. extend: BoolProperty(
  45. name="Extend",
  46. description="Extend the existing selection",
  47. default=True,
  48. )
  49. def execute(self, context):
  50. import fnmatch
  51. if self.case_sensitive:
  52. pattern_match = fnmatch.fnmatchcase
  53. else:
  54. pattern_match = (lambda a, b:
  55. fnmatch.fnmatchcase(a.upper(), b.upper()))
  56. is_ebone = False
  57. is_pbone = False
  58. obj = context.object
  59. if obj and obj.mode == 'POSE':
  60. items = obj.data.bones
  61. if not self.extend:
  62. bpy.ops.pose.select_all(action='DESELECT')
  63. is_pbone = True
  64. elif obj and obj.type == 'ARMATURE' and obj.mode == 'EDIT':
  65. items = obj.data.edit_bones
  66. if not self.extend:
  67. bpy.ops.armature.select_all(action='DESELECT')
  68. is_ebone = True
  69. else:
  70. items = context.visible_objects
  71. if not self.extend:
  72. bpy.ops.object.select_all(action='DESELECT')
  73. # Can be pose bones, edit bones or objects
  74. for item in items:
  75. if pattern_match(item.name, self.pattern):
  76. # hrmf, perhaps there should be a utility function for this.
  77. if is_ebone:
  78. item.select = True
  79. item.select_head = True
  80. item.select_tail = True
  81. if item.use_connect:
  82. item_parent = item.parent
  83. if item_parent is not None:
  84. item_parent.select_tail = True
  85. elif is_pbone:
  86. item.select = True
  87. else:
  88. item.select_set(True)
  89. return {'FINISHED'}
  90. def invoke(self, context, event):
  91. wm = context.window_manager
  92. return wm.invoke_props_popup(self, event)
  93. def draw(self, _context):
  94. layout = self.layout
  95. layout.prop(self, "pattern")
  96. row = layout.row()
  97. row.prop(self, "case_sensitive")
  98. row.prop(self, "extend")
  99. @classmethod
  100. def poll(cls, context):
  101. obj = context.object
  102. return (not obj) or (obj.mode == 'OBJECT') or (obj.type == 'ARMATURE')
  103. class SelectCamera(Operator):
  104. """Select the active camera"""
  105. bl_idname = "object.select_camera"
  106. bl_label = "Select Camera"
  107. bl_options = {'REGISTER', 'UNDO'}
  108. extend: BoolProperty(
  109. name="Extend",
  110. description="Extend the selection",
  111. default=False,
  112. )
  113. def execute(self, context):
  114. scene = context.scene
  115. view_layer = context.view_layer
  116. view = context.space_data
  117. if view.type == 'VIEW_3D' and view.use_local_camera:
  118. camera = view.camera
  119. else:
  120. camera = scene.camera
  121. if camera is None:
  122. self.report({'WARNING'}, "No camera found")
  123. elif camera.name not in scene.objects:
  124. self.report({'WARNING'}, "Active camera is not in this scene")
  125. else:
  126. if not self.extend:
  127. bpy.ops.object.select_all(action='DESELECT')
  128. view_layer.objects.active = camera
  129. # camera.hide = False # XXX TODO where is this now?
  130. camera.select_set(True)
  131. return {'FINISHED'}
  132. return {'CANCELLED'}
  133. class SelectHierarchy(Operator):
  134. """Select object relative to the active object's position """ \
  135. """in the hierarchy"""
  136. bl_idname = "object.select_hierarchy"
  137. bl_label = "Select Hierarchy"
  138. bl_options = {'REGISTER', 'UNDO'}
  139. direction: EnumProperty(
  140. items=(
  141. ('PARENT', "Parent", ""),
  142. ('CHILD', "Child", ""),
  143. ),
  144. name="Direction",
  145. description="Direction to select in the hierarchy",
  146. default='PARENT',
  147. )
  148. extend: BoolProperty(
  149. name="Extend",
  150. description="Extend the existing selection",
  151. default=False,
  152. )
  153. @classmethod
  154. def poll(cls, context):
  155. return context.object
  156. def execute(self, context):
  157. view_layer = context.view_layer
  158. select_new = []
  159. act_new = None
  160. selected_objects = context.selected_objects
  161. obj_act = context.object
  162. if context.object not in selected_objects:
  163. selected_objects.append(context.object)
  164. if self.direction == 'PARENT':
  165. for obj in selected_objects:
  166. parent = obj.parent
  167. if parent:
  168. if obj_act == obj:
  169. act_new = parent
  170. select_new.append(parent)
  171. else:
  172. for obj in selected_objects:
  173. select_new.extend(obj.children)
  174. if select_new:
  175. select_new.sort(key=lambda obj_iter: obj_iter.name)
  176. act_new = select_new[0]
  177. # don't edit any object settings above this
  178. if select_new:
  179. if not self.extend:
  180. bpy.ops.object.select_all(action='DESELECT')
  181. for obj in select_new:
  182. obj.select_set(True)
  183. view_layer.objects.active = act_new
  184. return {'FINISHED'}
  185. return {'CANCELLED'}
  186. class SubdivisionSet(Operator):
  187. """Sets a Subdivision Surface Level (1-5)"""
  188. bl_idname = "object.subdivision_set"
  189. bl_label = "Subdivision Set"
  190. bl_options = {'REGISTER', 'UNDO'}
  191. level: IntProperty(
  192. name="Level",
  193. min=-100, max=100,
  194. soft_min=-6, soft_max=6,
  195. default=1,
  196. )
  197. relative: BoolProperty(
  198. name="Relative",
  199. description=("Apply the subsurf level as an offset "
  200. "relative to the current level"),
  201. default=False,
  202. )
  203. @classmethod
  204. def poll(cls, context):
  205. obs = context.selected_editable_objects
  206. return (obs is not None)
  207. def execute(self, context):
  208. level = self.level
  209. relative = self.relative
  210. if relative and level == 0:
  211. return {'CANCELLED'} # nothing to do
  212. if not relative and level < 0:
  213. self.level = level = 0
  214. def set_object_subd(obj):
  215. for mod in obj.modifiers:
  216. if mod.type == 'MULTIRES':
  217. if not relative:
  218. if level > mod.total_levels:
  219. sub = level - mod.total_levels
  220. for _ in range(sub):
  221. bpy.ops.object.multires_subdivide(modifier="Multires")
  222. if obj.mode == 'SCULPT':
  223. if mod.sculpt_levels != level:
  224. mod.sculpt_levels = level
  225. elif obj.mode == 'OBJECT':
  226. if mod.levels != level:
  227. mod.levels = level
  228. return
  229. else:
  230. if obj.mode == 'SCULPT':
  231. if mod.sculpt_levels + level <= mod.total_levels:
  232. mod.sculpt_levels += level
  233. elif obj.mode == 'OBJECT':
  234. if mod.levels + level <= mod.total_levels:
  235. mod.levels += level
  236. return
  237. elif mod.type == 'SUBSURF':
  238. if relative:
  239. mod.levels += level
  240. else:
  241. if mod.levels != level:
  242. mod.levels = level
  243. return
  244. # add a new modifier
  245. try:
  246. if obj.mode == 'SCULPT':
  247. mod = obj.modifiers.new("Multires", 'MULTIRES')
  248. if level > 0:
  249. for _ in range(level):
  250. bpy.ops.object.multires_subdivide(modifier="Multires")
  251. else:
  252. mod = obj.modifiers.new("Subdivision", 'SUBSURF')
  253. mod.levels = level
  254. except:
  255. self.report({'WARNING'},
  256. "Modifiers cannot be added to object: " + obj.name)
  257. for obj in context.selected_editable_objects:
  258. set_object_subd(obj)
  259. return {'FINISHED'}
  260. class ShapeTransfer(Operator):
  261. """Copy the active shape key of another selected object to this one"""
  262. bl_idname = "object.shape_key_transfer"
  263. bl_label = "Transfer Shape Key"
  264. bl_options = {'REGISTER', 'UNDO'}
  265. mode: EnumProperty(
  266. items=(
  267. ('OFFSET',
  268. "Offset",
  269. "Apply the relative positional offset",
  270. ),
  271. ('RELATIVE_FACE',
  272. "Relative Face",
  273. "Calculate relative position (using faces)",
  274. ),
  275. ('RELATIVE_EDGE',
  276. "Relative Edge",
  277. "Calculate relative position (using edges)",
  278. ),
  279. ),
  280. name="Transformation Mode",
  281. description="Relative shape positions to the new shape method",
  282. default='OFFSET',
  283. )
  284. use_clamp: BoolProperty(
  285. name="Clamp Offset",
  286. description=("Clamp the transformation to the distance each "
  287. "vertex moves in the original shape"),
  288. default=False,
  289. )
  290. def _main(self, ob_act, objects, mode='OFFSET', use_clamp=False):
  291. def me_nos(verts):
  292. return [v.normal.copy() for v in verts]
  293. def me_cos(verts):
  294. return [v.co.copy() for v in verts]
  295. def ob_add_shape(ob, name):
  296. me = ob.data
  297. key = ob.shape_key_add(from_mix=False)
  298. if len(me.shape_keys.key_blocks) == 1:
  299. key.name = "Basis"
  300. key = ob.shape_key_add(from_mix=False) # we need a rest
  301. key.name = name
  302. ob.active_shape_key_index = len(me.shape_keys.key_blocks) - 1
  303. ob.show_only_shape_key = True
  304. from mathutils.geometry import barycentric_transform
  305. from mathutils import Vector
  306. if use_clamp and mode == 'OFFSET':
  307. use_clamp = False
  308. me = ob_act.data
  309. orig_key_name = ob_act.active_shape_key.name
  310. orig_shape_coords = me_cos(ob_act.active_shape_key.data)
  311. orig_normals = me_nos(me.vertices)
  312. # actual mesh vertex location isn't as reliable as the base shape :S
  313. # orig_coords = me_cos(me.vertices)
  314. orig_coords = me_cos(me.shape_keys.key_blocks[0].data)
  315. for ob_other in objects:
  316. if ob_other.type != 'MESH':
  317. self.report({'WARNING'},
  318. ("Skipping '%s', "
  319. "not a mesh") % ob_other.name)
  320. continue
  321. me_other = ob_other.data
  322. if len(me_other.vertices) != len(me.vertices):
  323. self.report({'WARNING'},
  324. ("Skipping '%s', "
  325. "vertex count differs") % ob_other.name)
  326. continue
  327. target_normals = me_nos(me_other.vertices)
  328. if me_other.shape_keys:
  329. target_coords = me_cos(me_other.shape_keys.key_blocks[0].data)
  330. else:
  331. target_coords = me_cos(me_other.vertices)
  332. ob_add_shape(ob_other, orig_key_name)
  333. # editing the final coords, only list that stores wrapped coords
  334. target_shape_coords = [v.co for v in
  335. ob_other.active_shape_key.data]
  336. median_coords = [[] for i in range(len(me.vertices))]
  337. # Method 1, edge
  338. if mode == 'OFFSET':
  339. for i, vert_cos in enumerate(median_coords):
  340. vert_cos.append(target_coords[i] +
  341. (orig_shape_coords[i] - orig_coords[i]))
  342. elif mode == 'RELATIVE_FACE':
  343. for poly in me.polygons:
  344. idxs = poly.vertices[:]
  345. v_before = idxs[-2]
  346. v = idxs[-1]
  347. for v_after in idxs:
  348. pt = barycentric_transform(orig_shape_coords[v],
  349. orig_coords[v_before],
  350. orig_coords[v],
  351. orig_coords[v_after],
  352. target_coords[v_before],
  353. target_coords[v],
  354. target_coords[v_after],
  355. )
  356. median_coords[v].append(pt)
  357. v_before = v
  358. v = v_after
  359. elif mode == 'RELATIVE_EDGE':
  360. for ed in me.edges:
  361. i1, i2 = ed.vertices
  362. v1, v2 = orig_coords[i1], orig_coords[i2]
  363. edge_length = (v1 - v2).length
  364. n1loc = v1 + orig_normals[i1] * edge_length
  365. n2loc = v2 + orig_normals[i2] * edge_length
  366. # now get the target nloc's
  367. v1_to, v2_to = target_coords[i1], target_coords[i2]
  368. edlen_to = (v1_to - v2_to).length
  369. n1loc_to = v1_to + target_normals[i1] * edlen_to
  370. n2loc_to = v2_to + target_normals[i2] * edlen_to
  371. pt = barycentric_transform(orig_shape_coords[i1],
  372. v2, v1, n1loc,
  373. v2_to, v1_to, n1loc_to)
  374. median_coords[i1].append(pt)
  375. pt = barycentric_transform(orig_shape_coords[i2],
  376. v1, v2, n2loc,
  377. v1_to, v2_to, n2loc_to)
  378. median_coords[i2].append(pt)
  379. # apply the offsets to the new shape
  380. from functools import reduce
  381. VectorAdd = Vector.__add__
  382. for i, vert_cos in enumerate(median_coords):
  383. if vert_cos:
  384. co = reduce(VectorAdd, vert_cos) / len(vert_cos)
  385. if use_clamp:
  386. # clamp to the same movement as the original
  387. # breaks copy between different scaled meshes.
  388. len_from = (orig_shape_coords[i] -
  389. orig_coords[i]).length
  390. ofs = co - target_coords[i]
  391. ofs.length = len_from
  392. co = target_coords[i] + ofs
  393. target_shape_coords[i][:] = co
  394. return {'FINISHED'}
  395. @classmethod
  396. def poll(cls, context):
  397. obj = context.active_object
  398. return (obj and obj.mode != 'EDIT')
  399. def execute(self, context):
  400. ob_act = context.active_object
  401. objects = [ob for ob in context.selected_editable_objects
  402. if ob != ob_act]
  403. if 1: # swap from/to, means we can't copy to many at once.
  404. if len(objects) != 1:
  405. self.report({'ERROR'},
  406. ("Expected one other selected "
  407. "mesh object to copy from"))
  408. return {'CANCELLED'}
  409. ob_act, objects = objects[0], [ob_act]
  410. if ob_act.type != 'MESH':
  411. self.report({'ERROR'}, "Other object is not a mesh")
  412. return {'CANCELLED'}
  413. if ob_act.active_shape_key is None:
  414. self.report({'ERROR'}, "Other object has no shape key")
  415. return {'CANCELLED'}
  416. return self._main(ob_act, objects, self.mode, self.use_clamp)
  417. class JoinUVs(Operator):
  418. """Transfer UV Maps from active to selected objects """ \
  419. """(needs matching geometry)"""
  420. bl_idname = "object.join_uvs"
  421. bl_label = "Transfer UV Maps"
  422. bl_options = {'REGISTER', 'UNDO'}
  423. @classmethod
  424. def poll(cls, context):
  425. obj = context.active_object
  426. return (obj and obj.type == 'MESH')
  427. def _main(self, context):
  428. import array
  429. obj = context.active_object
  430. mesh = obj.data
  431. is_editmode = (obj.mode == 'EDIT')
  432. if is_editmode:
  433. bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
  434. if not mesh.uv_layers:
  435. self.report({'WARNING'},
  436. "Object: %s, Mesh: '%s' has no UVs"
  437. % (obj.name, mesh.name))
  438. else:
  439. nbr_loops = len(mesh.loops)
  440. # seems to be the fastest way to create an array
  441. uv_array = array.array('f', [0.0] * 2) * nbr_loops
  442. mesh.uv_layers.active.data.foreach_get("uv", uv_array)
  443. objects = context.selected_editable_objects[:]
  444. for obj_other in objects:
  445. if obj_other.type == 'MESH':
  446. obj_other.data.tag = False
  447. for obj_other in objects:
  448. if obj_other != obj and obj_other.type == 'MESH':
  449. mesh_other = obj_other.data
  450. if mesh_other != mesh:
  451. if mesh_other.tag is False:
  452. mesh_other.tag = True
  453. if len(mesh_other.loops) != nbr_loops:
  454. self.report({'WARNING'}, "Object: %s, Mesh: "
  455. "'%s' has %d loops (for %d faces),"
  456. " expected %d\n"
  457. % (obj_other.name,
  458. mesh_other.name,
  459. len(mesh_other.loops),
  460. len(mesh_other.polygons),
  461. nbr_loops,
  462. ),
  463. )
  464. else:
  465. uv_other = mesh_other.uv_layers.active
  466. if not uv_other:
  467. mesh_other.uv_layers.new()
  468. uv_other = mesh_other.uv_layers.active
  469. if not uv_other:
  470. self.report({'ERROR'}, "Could not add "
  471. "a new UV map tp object "
  472. "'%s' (Mesh '%s')\n"
  473. % (obj_other.name,
  474. mesh_other.name,
  475. ),
  476. )
  477. # finally do the copy
  478. uv_other.data.foreach_set("uv", uv_array)
  479. if is_editmode:
  480. bpy.ops.object.mode_set(mode='EDIT', toggle=False)
  481. def execute(self, context):
  482. self._main(context)
  483. return {'FINISHED'}
  484. class MakeDupliFace(Operator):
  485. """Convert objects into instanced faces"""
  486. bl_idname = "object.make_dupli_face"
  487. bl_label = "Make Instance Face"
  488. bl_options = {'REGISTER', 'UNDO'}
  489. @staticmethod
  490. def _main(context):
  491. from mathutils import Vector
  492. from collections import defaultdict
  493. SCALE_FAC = 0.01
  494. offset = 0.5 * SCALE_FAC
  495. base_tri = (Vector((-offset, -offset, 0.0)),
  496. Vector((+offset, -offset, 0.0)),
  497. Vector((+offset, +offset, 0.0)),
  498. Vector((-offset, +offset, 0.0)),
  499. )
  500. def matrix_to_quad(matrix):
  501. # scale = matrix.median_scale
  502. trans = matrix.to_translation()
  503. rot = matrix.to_3x3() # also contains scale
  504. return [(rot @ b) + trans for b in base_tri]
  505. linked = defaultdict(list)
  506. for obj in context.selected_objects:
  507. if obj.type == 'MESH':
  508. linked[obj.data].append(obj)
  509. for data, objects in linked.items():
  510. face_verts = [axis for obj in objects
  511. for v in matrix_to_quad(obj.matrix_world)
  512. for axis in v]
  513. nbr_verts = len(face_verts) // 3
  514. nbr_faces = nbr_verts // 4
  515. faces = list(range(nbr_verts))
  516. mesh = bpy.data.meshes.new(data.name + "_dupli")
  517. mesh.vertices.add(nbr_verts)
  518. mesh.loops.add(nbr_faces * 4) # Safer than nbr_verts.
  519. mesh.polygons.add(nbr_faces)
  520. mesh.vertices.foreach_set("co", face_verts)
  521. mesh.loops.foreach_set("vertex_index", faces)
  522. mesh.polygons.foreach_set("loop_start", range(0, nbr_faces * 4, 4))
  523. mesh.polygons.foreach_set("loop_total", (4,) * nbr_faces)
  524. mesh.update() # generates edge data
  525. ob_new = bpy.data.objects.new(mesh.name, mesh)
  526. context.collection.objects.link(ob_new)
  527. ob_inst = bpy.data.objects.new(data.name, data)
  528. context.collection.objects.link(ob_inst)
  529. ob_new.instance_type = 'FACES'
  530. ob_inst.parent = ob_new
  531. ob_new.use_instance_faces_scale = True
  532. ob_new.instance_faces_scale = 1.0 / SCALE_FAC
  533. ob_inst.select_set(True)
  534. ob_new.select_set(True)
  535. for obj in objects:
  536. for collection in obj.users_collection:
  537. collection.objects.unlink(obj)
  538. def execute(self, context):
  539. self._main(context)
  540. return {'FINISHED'}
  541. class IsolateTypeRender(Operator):
  542. """Hide unselected render objects of same type as active """ \
  543. """by setting the hide render flag"""
  544. bl_idname = "object.isolate_type_render"
  545. bl_label = "Restrict Render Unselected"
  546. bl_options = {'REGISTER', 'UNDO'}
  547. def execute(self, context):
  548. act_type = context.object.type
  549. for obj in context.visible_objects:
  550. if obj.select_get():
  551. obj.hide_render = False
  552. else:
  553. if obj.type == act_type:
  554. obj.hide_render = True
  555. return {'FINISHED'}
  556. class ClearAllRestrictRender(Operator):
  557. """Reveal all render objects by setting the hide render flag"""
  558. bl_idname = "object.hide_render_clear_all"
  559. bl_label = "Clear All Restrict Render"
  560. bl_options = {'REGISTER', 'UNDO'}
  561. def execute(self, context):
  562. for obj in context.scene.objects:
  563. obj.hide_render = False
  564. return {'FINISHED'}
  565. class TransformsToDeltas(Operator):
  566. """Convert normal object transforms to delta transforms, """ \
  567. """any existing delta transforms will be included as well"""
  568. bl_idname = "object.transforms_to_deltas"
  569. bl_label = "Transforms to Deltas"
  570. bl_options = {'REGISTER', 'UNDO'}
  571. mode: EnumProperty(
  572. items=(
  573. ('ALL', "All Transforms", "Transfer location, rotation, and scale transforms"),
  574. ('LOC', "Location", "Transfer location transforms only"),
  575. ('ROT', "Rotation", "Transfer rotation transforms only"),
  576. ('SCALE', "Scale", "Transfer scale transforms only"),
  577. ),
  578. name="Mode",
  579. description="Which transforms to transfer",
  580. default='ALL',
  581. )
  582. reset_values: BoolProperty(
  583. name="Reset Values",
  584. description=("Clear transform values after transferring to deltas"),
  585. default=True,
  586. )
  587. @classmethod
  588. def poll(cls, context):
  589. obs = context.selected_editable_objects
  590. return (obs is not None)
  591. def execute(self, context):
  592. for obj in context.selected_editable_objects:
  593. if self.mode in {'ALL', 'LOC'}:
  594. self.transfer_location(obj)
  595. if self.mode in {'ALL', 'ROT'}:
  596. self.transfer_rotation(obj)
  597. if self.mode in {'ALL', 'SCALE'}:
  598. self.transfer_scale(obj)
  599. return {'FINISHED'}
  600. def transfer_location(self, obj):
  601. obj.delta_location += obj.location
  602. if self.reset_values:
  603. obj.location.zero()
  604. def transfer_rotation(self, obj):
  605. # TODO: add transforms together...
  606. if obj.rotation_mode == 'QUATERNION':
  607. obj.delta_rotation_quaternion += obj.rotation_quaternion
  608. if self.reset_values:
  609. obj.rotation_quaternion.identity()
  610. elif obj.rotation_mode == 'AXIS_ANGLE':
  611. pass # Unsupported
  612. else:
  613. delta = obj.delta_rotation_euler.copy()
  614. obj.delta_rotation_euler = obj.rotation_euler
  615. obj.delta_rotation_euler.rotate(delta)
  616. if self.reset_values:
  617. obj.rotation_euler.zero()
  618. def transfer_scale(self, obj):
  619. obj.delta_scale[0] *= obj.scale[0]
  620. obj.delta_scale[1] *= obj.scale[1]
  621. obj.delta_scale[2] *= obj.scale[2]
  622. if self.reset_values:
  623. obj.scale[:] = (1, 1, 1)
  624. class TransformsToDeltasAnim(Operator):
  625. """Convert object animation for normal transforms to delta transforms"""
  626. bl_idname = "object.anim_transforms_to_deltas"
  627. bl_label = "Animated Transforms to Deltas"
  628. bl_options = {'REGISTER', 'UNDO'}
  629. @classmethod
  630. def poll(cls, context):
  631. obs = context.selected_editable_objects
  632. return (obs is not None)
  633. def execute(self, context):
  634. # map from standard transform paths to "new" transform paths
  635. STANDARD_TO_DELTA_PATHS = {
  636. "location": "delta_location",
  637. "rotation_euler": "delta_rotation_euler",
  638. "rotation_quaternion": "delta_rotation_quaternion",
  639. # "rotation_axis_angle" : "delta_rotation_axis_angle",
  640. "scale": "delta_scale"
  641. }
  642. DELTA_PATHS = STANDARD_TO_DELTA_PATHS.values()
  643. # try to apply on each selected object
  644. for obj in context.selected_editable_objects:
  645. adt = obj.animation_data
  646. if (adt is None) or (adt.action is None):
  647. self.report({'WARNING'},
  648. "No animation data to convert on object: %r" %
  649. obj.name)
  650. continue
  651. # first pass over F-Curves: ensure that we don't have conflicting
  652. # transforms already (e.g. if this was applied already) [#29110]
  653. existingFCurves = {}
  654. for fcu in adt.action.fcurves:
  655. # get "delta" path - i.e. the final paths which may clash
  656. path = fcu.data_path
  657. if path in STANDARD_TO_DELTA_PATHS:
  658. # to be converted - conflicts may exist...
  659. dpath = STANDARD_TO_DELTA_PATHS[path]
  660. elif path in DELTA_PATHS:
  661. # already delta - check for conflicts...
  662. dpath = path
  663. else:
  664. # non-transform - ignore
  665. continue
  666. # a delta path like this for the same index shouldn't
  667. # exist already, otherwise we've got a conflict
  668. if dpath in existingFCurves:
  669. # ensure that this index hasn't occurred before
  670. if fcu.array_index in existingFCurves[dpath]:
  671. # conflict
  672. self.report({'ERROR'},
  673. "Object '%r' already has '%r' F-Curve(s). "
  674. "Remove these before trying again" %
  675. (obj.name, dpath))
  676. return {'CANCELLED'}
  677. else:
  678. # no conflict here
  679. existingFCurves[dpath] += [fcu.array_index]
  680. else:
  681. # no conflict yet
  682. existingFCurves[dpath] = [fcu.array_index]
  683. # if F-Curve uses standard transform path
  684. # just append "delta_" to this path
  685. for fcu in adt.action.fcurves:
  686. if fcu.data_path == "location":
  687. fcu.data_path = "delta_location"
  688. obj.location.zero()
  689. elif fcu.data_path == "rotation_euler":
  690. fcu.data_path = "delta_rotation_euler"
  691. obj.rotation_euler.zero()
  692. elif fcu.data_path == "rotation_quaternion":
  693. fcu.data_path = "delta_rotation_quaternion"
  694. obj.rotation_quaternion.identity()
  695. # XXX: currently not implemented
  696. # ~ elif fcu.data_path == "rotation_axis_angle":
  697. # ~ fcu.data_path = "delta_rotation_axis_angle"
  698. elif fcu.data_path == "scale":
  699. fcu.data_path = "delta_scale"
  700. obj.scale = 1.0, 1.0, 1.0
  701. # hack: force animsys flush by changing frame, so that deltas get run
  702. context.scene.frame_set(context.scene.frame_current)
  703. return {'FINISHED'}
  704. class DupliOffsetFromCursor(Operator):
  705. """Set offset used for collection instances based on cursor position"""
  706. bl_idname = "object.instance_offset_from_cursor"
  707. bl_label = "Set Offset From Cursor"
  708. bl_options = {'INTERNAL', 'UNDO'}
  709. @classmethod
  710. def poll(cls, context):
  711. return (context.active_object is not None)
  712. def execute(self, context):
  713. scene = context.scene
  714. collection = context.collection
  715. collection.instance_offset = scene.cursor.location
  716. return {'FINISHED'}
  717. class LoadImageAsEmpty:
  718. bl_options = {'REGISTER', 'UNDO'}
  719. filepath: StringProperty(
  720. subtype='FILE_PATH'
  721. )
  722. filter_image: BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
  723. filter_folder: BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
  724. view_align: BoolProperty(
  725. name="Align to view",
  726. default=True,
  727. )
  728. @classmethod
  729. def poll(cls, context):
  730. return context.mode == 'OBJECT'
  731. def invoke(self, context, _event):
  732. context.window_manager.fileselect_add(self)
  733. return {'RUNNING_MODAL'}
  734. def execute(self, context):
  735. scene = context.scene
  736. cursor = scene.cursor.location
  737. try:
  738. image = bpy.data.images.load(self.filepath, check_existing=True)
  739. except RuntimeError as ex:
  740. self.report({'ERROR'}, str(ex))
  741. return {'CANCELLED'}
  742. bpy.ops.object.empty_add(
  743. 'INVOKE_REGION_WIN',
  744. type='IMAGE',
  745. location=cursor,
  746. align=('VIEW' if self.view_align else 'WORLD'),
  747. )
  748. obj = context.active_object
  749. obj.data = image
  750. obj.empty_display_size = 5.0
  751. self.set_settings(context, obj)
  752. return {'FINISHED'}
  753. def set_settings(self, context, obj):
  754. pass
  755. class LoadBackgroundImage(LoadImageAsEmpty, Operator):
  756. """Add a reference image into the background behind objects"""
  757. bl_idname = "object.load_background_image"
  758. bl_label = "Load Background Image"
  759. def set_settings(self, context, obj):
  760. obj.empty_image_depth = 'BACK'
  761. obj.empty_image_side = 'FRONT'
  762. if context.space_data.type == 'VIEW_3D':
  763. if not context.space_data.region_3d.is_perspective:
  764. obj.show_empty_image_perspective = False
  765. class LoadReferenceImage(LoadImageAsEmpty, Operator):
  766. """Add a reference image into the scene between objects"""
  767. bl_idname = "object.load_reference_image"
  768. bl_label = "Load Reference Image"
  769. def set_settings(self, context, obj):
  770. pass
  771. class OBJECT_OT_assign_property_defaults(Operator):
  772. """Assign the current values of custom properties as their defaults, """ \
  773. """for use as part of the rest pose state in NLA track mixing"""
  774. bl_idname = "object.assign_property_defaults"
  775. bl_label = "Assign Custom Property Values as Default"
  776. bl_options = {'UNDO', 'REGISTER'}
  777. process_data: BoolProperty(name="Process data properties", default=True)
  778. process_bones: BoolProperty(name="Process bone properties", default=True)
  779. @classmethod
  780. def poll(cls, context):
  781. obj = context.active_object
  782. return obj is not None and obj.library is None and obj.mode in {'POSE', 'OBJECT'}
  783. @staticmethod
  784. def assign_defaults(obj):
  785. from rna_prop_ui import rna_idprop_ui_prop_default_set
  786. rna_properties = {'_RNA_UI'} | {prop.identifier for prop in obj.bl_rna.properties if prop.is_runtime}
  787. for prop, value in obj.items():
  788. if prop not in rna_properties:
  789. rna_idprop_ui_prop_default_set(obj, prop, value)
  790. def execute(self, context):
  791. obj = context.active_object
  792. self.assign_defaults(obj)
  793. if self.process_bones and obj.pose:
  794. for pbone in obj.pose.bones:
  795. self.assign_defaults(pbone)
  796. if self.process_data and obj.data and obj.data.library is None:
  797. self.assign_defaults(obj.data)
  798. if self.process_bones and isinstance(obj.data, bpy.types.Armature):
  799. for bone in obj.data.bones:
  800. self.assign_defaults(bone)
  801. return {'FINISHED'}
  802. classes = (
  803. ClearAllRestrictRender,
  804. DupliOffsetFromCursor,
  805. IsolateTypeRender,
  806. JoinUVs,
  807. LoadBackgroundImage,
  808. LoadReferenceImage,
  809. MakeDupliFace,
  810. SelectCamera,
  811. SelectHierarchy,
  812. SelectPattern,
  813. ShapeTransfer,
  814. SubdivisionSet,
  815. TransformsToDeltas,
  816. TransformsToDeltasAnim,
  817. OBJECT_OT_assign_property_defaults,
  818. )