uvcalc_follow_active.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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 compliant>
  19. # for full docs see...
  20. # https://docs.blender.org/manual/en/latest/editors/uv_image/uv/editing/unwrapping/mapping_types.html#follow-active-quads
  21. import bpy
  22. from bpy.types import Operator
  23. STATUS_OK = (1 << 0)
  24. STATUS_ERR_ACTIVE_FACE = (1 << 1)
  25. STATUS_ERR_NOT_SELECTED = (1 << 2)
  26. STATUS_ERR_NOT_QUAD = (1 << 3)
  27. def extend(obj, EXTEND_MODE):
  28. import bmesh
  29. me = obj.data
  30. bm = bmesh.from_edit_mesh(me)
  31. faces = [f for f in bm.faces if f.select and len(f.verts) == 4]
  32. if not faces:
  33. return 0
  34. f_act = bm.faces.active
  35. if f_act is None:
  36. return STATUS_ERR_ACTIVE_FACE
  37. if not f_act.select:
  38. return STATUS_ERR_NOT_SELECTED
  39. elif len(f_act.verts) != 4:
  40. return STATUS_ERR_NOT_QUAD
  41. # Script will fail without UVs.
  42. if not me.uv_layers:
  43. me.uv_layers.new()
  44. uv_act = bm.loops.layers.uv.active
  45. # our own local walker
  46. def walk_face_init(faces, f_act):
  47. # first tag all faces True (so we don't uvmap them)
  48. for f in bm.faces:
  49. f.tag = True
  50. # then tag faces arg False
  51. for f in faces:
  52. f.tag = False
  53. # tag the active face True since we begin there
  54. f_act.tag = True
  55. def walk_face(f):
  56. # all faces in this list must be tagged
  57. f.tag = True
  58. faces_a = [f]
  59. faces_b = []
  60. while faces_a:
  61. for f in faces_a:
  62. for l in f.loops:
  63. l_edge = l.edge
  64. if (l_edge.is_manifold is True) and (l_edge.seam is False):
  65. l_other = l.link_loop_radial_next
  66. f_other = l_other.face
  67. if not f_other.tag:
  68. yield (f, l, f_other)
  69. f_other.tag = True
  70. faces_b.append(f_other)
  71. # swap
  72. faces_a, faces_b = faces_b, faces_a
  73. faces_b.clear()
  74. def walk_edgeloop(l):
  75. """
  76. Could make this a generic function
  77. """
  78. e_first = l.edge
  79. e = None
  80. while True:
  81. e = l.edge
  82. yield e
  83. # don't step past non-manifold edges
  84. if e.is_manifold:
  85. # welk around the quad and then onto the next face
  86. l = l.link_loop_radial_next
  87. if len(l.face.verts) == 4:
  88. l = l.link_loop_next.link_loop_next
  89. if l.edge is e_first:
  90. break
  91. else:
  92. break
  93. else:
  94. break
  95. def extrapolate_uv(
  96. fac,
  97. l_a_outer, l_a_inner,
  98. l_b_outer, l_b_inner,
  99. ):
  100. l_b_inner[:] = l_a_inner
  101. l_b_outer[:] = l_a_inner + ((l_a_inner - l_a_outer) * fac)
  102. def apply_uv(_f_prev, l_prev, _f_next):
  103. l_a = [None, None, None, None]
  104. l_b = [None, None, None, None]
  105. l_a[0] = l_prev
  106. l_a[1] = l_a[0].link_loop_next
  107. l_a[2] = l_a[1].link_loop_next
  108. l_a[3] = l_a[2].link_loop_next
  109. # l_b
  110. # +-----------+
  111. # |(3) |(2)
  112. # | |
  113. # |l_next(0) |(1)
  114. # +-----------+
  115. # ^
  116. # l_a |
  117. # +-----------+
  118. # |l_prev(0) |(1)
  119. # | (f) |
  120. # |(3) |(2)
  121. # +-----------+
  122. # copy from this face to the one above.
  123. # get the other loops
  124. l_next = l_prev.link_loop_radial_next
  125. if l_next.vert != l_prev.vert:
  126. l_b[1] = l_next
  127. l_b[0] = l_b[1].link_loop_next
  128. l_b[3] = l_b[0].link_loop_next
  129. l_b[2] = l_b[3].link_loop_next
  130. else:
  131. l_b[0] = l_next
  132. l_b[1] = l_b[0].link_loop_next
  133. l_b[2] = l_b[1].link_loop_next
  134. l_b[3] = l_b[2].link_loop_next
  135. l_a_uv = [l[uv_act].uv for l in l_a]
  136. l_b_uv = [l[uv_act].uv for l in l_b]
  137. if EXTEND_MODE == 'LENGTH_AVERAGE':
  138. fac = edge_lengths[l_b[2].edge.index][0] / edge_lengths[l_a[1].edge.index][0]
  139. elif EXTEND_MODE == 'LENGTH':
  140. a0, b0, c0 = l_a[3].vert.co, l_a[0].vert.co, l_b[3].vert.co
  141. a1, b1, c1 = l_a[2].vert.co, l_a[1].vert.co, l_b[2].vert.co
  142. d1 = (a0 - b0).length + (a1 - b1).length
  143. d2 = (b0 - c0).length + (b1 - c1).length
  144. try:
  145. fac = d2 / d1
  146. except ZeroDivisionError:
  147. fac = 1.0
  148. else:
  149. fac = 1.0
  150. extrapolate_uv(fac,
  151. l_a_uv[3], l_a_uv[0],
  152. l_b_uv[3], l_b_uv[0])
  153. extrapolate_uv(fac,
  154. l_a_uv[2], l_a_uv[1],
  155. l_b_uv[2], l_b_uv[1])
  156. # -------------------------------------------
  157. # Calculate average length per loop if needed
  158. if EXTEND_MODE == 'LENGTH_AVERAGE':
  159. bm.edges.index_update()
  160. edge_lengths = [None] * len(bm.edges)
  161. for f in faces:
  162. # we know its a quad
  163. l_quad = f.loops[:]
  164. l_pair_a = (l_quad[0], l_quad[2])
  165. l_pair_b = (l_quad[1], l_quad[3])
  166. for l_pair in (l_pair_a, l_pair_b):
  167. if edge_lengths[l_pair[0].edge.index] is None:
  168. edge_length_store = [-1.0]
  169. edge_length_accum = 0.0
  170. edge_length_total = 0
  171. for l in l_pair:
  172. if edge_lengths[l.edge.index] is None:
  173. for e in walk_edgeloop(l):
  174. if edge_lengths[e.index] is None:
  175. edge_lengths[e.index] = edge_length_store
  176. edge_length_accum += e.calc_length()
  177. edge_length_total += 1
  178. edge_length_store[0] = edge_length_accum / edge_length_total
  179. # done with average length
  180. # ------------------------
  181. walk_face_init(faces, f_act)
  182. for f_triple in walk_face(f_act):
  183. apply_uv(*f_triple)
  184. bmesh.update_edit_mesh(me, False)
  185. return STATUS_OK
  186. def main(context, operator):
  187. num_meshes = 0
  188. num_errors = 0
  189. status = 0
  190. ob_list = context.objects_in_mode_unique_data
  191. for ob in ob_list:
  192. num_meshes += 1
  193. ret = extend(ob, operator.properties.mode)
  194. if ret != STATUS_OK:
  195. num_errors += 1
  196. status |= ret
  197. if num_errors == num_meshes:
  198. if status & STATUS_ERR_NOT_QUAD:
  199. operator.report({'ERROR'}, "Active face must be a quad")
  200. elif status & STATUS_ERR_NOT_SELECTED:
  201. operator.report({'ERROR'}, "Active face not selected")
  202. else:
  203. assert((status & STATUS_ERR_ACTIVE_FACE) != 0)
  204. operator.report({'ERROR'}, "No active face")
  205. class FollowActiveQuads(Operator):
  206. """Follow UVs from active quads along continuous face loops"""
  207. bl_idname = "uv.follow_active_quads"
  208. bl_label = "Follow Active Quads"
  209. bl_options = {'REGISTER', 'UNDO'}
  210. mode: bpy.props.EnumProperty(
  211. name="Edge Length Mode",
  212. description="Method to space UV edge loops",
  213. items=(
  214. ('EVEN', "Even", "Space all UVs evenly"),
  215. ('LENGTH', "Length", "Average space UVs edge length of each loop"),
  216. ('LENGTH_AVERAGE', "Length Average", "Average space UVs edge length of each loop"),
  217. ),
  218. default='LENGTH_AVERAGE',
  219. )
  220. @classmethod
  221. def poll(cls, context):
  222. return context.mode == 'EDIT_MESH'
  223. def execute(self, context):
  224. main(context, self)
  225. return {'FINISHED'}
  226. def invoke(self, context, _event):
  227. wm = context.window_manager
  228. return wm.invoke_props_dialog(self)
  229. classes = (
  230. FollowActiveQuads,
  231. )