Update for 2.8
[blender-addons.git] / uv_magic_uv / op / transfer_uv.py
blob132f395eda81c291bda778d6f14a7c1ce85c3df9
1 # <pep8-80 compliant>
3 # ##### BEGIN GPL LICENSE BLOCK #####
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # ##### END GPL LICENSE BLOCK #####
21 __author__ = "Nutti <nutti.metro@gmail.com>, Mifth, MaxRobinot"
22 __status__ = "production"
23 __version__ = "5.1"
24 __date__ = "24 Feb 2018"
26 from collections import OrderedDict
28 import bpy
29 import bmesh
30 from bpy.props import BoolProperty
32 from .. import common
35 class MUV_TransUVCopy(bpy.types.Operator):
36 """
37 Operation class: Transfer UV copy
38 Topological based copy
39 """
41 bl_idname = "uv.muv_transuv_copy"
42 bl_label = "Transfer UV Copy"
43 bl_description = "Transfer UV Copy (Topological based copy)"
44 bl_options = {'REGISTER', 'UNDO'}
46 def execute(self, context):
47 props = context.scene.muv_props.transuv
48 active_obj = context.scene.objects.active
49 bm = bmesh.from_edit_mesh(active_obj.data)
50 if common.check_version(2, 73, 0) >= 0:
51 bm.faces.ensure_lookup_table()
53 # get UV layer
54 if not bm.loops.layers.uv:
55 self.report({'WARNING'}, "Object must have more than one UV map")
56 return {'CANCELLED'}
57 uv_layer = bm.loops.layers.uv.verify()
59 props.topology_copied.clear()
61 # get selected faces
62 active_face = bm.faces.active
63 sel_faces = [face for face in bm.faces if face.select]
64 if len(sel_faces) != 2:
65 self.report({'WARNING'}, "Two faces must be selected")
66 return {'CANCELLED'}
67 if not active_face or active_face not in sel_faces:
68 self.report({'WARNING'}, "Two faces must be active")
69 return {'CANCELLED'}
71 # parse all faces according to selection
72 active_face_nor = active_face.normal.copy()
73 all_sorted_faces = main_parse(
74 self, uv_layer, sel_faces, active_face,
75 active_face_nor)
77 if all_sorted_faces:
78 for face_data in all_sorted_faces.values():
79 edges = face_data[1]
80 uv_loops = face_data[2]
81 uvs = [l.uv.copy() for l in uv_loops]
82 pin_uvs = [l.pin_uv for l in uv_loops]
83 seams = [e.seam for e in edges]
84 props.topology_copied.append([uvs, pin_uvs, seams])
86 bmesh.update_edit_mesh(active_obj.data)
88 return {'FINISHED'}
91 class MUV_TransUVPaste(bpy.types.Operator):
92 """
93 Operation class: Transfer UV paste
94 Topological based paste
95 """
97 bl_idname = "uv.muv_transuv_paste"
98 bl_label = "Transfer UV Paste"
99 bl_description = "Transfer UV Paste (Topological based paste)"
100 bl_options = {'REGISTER', 'UNDO'}
102 invert_normals = BoolProperty(
103 name="Invert Normals",
104 description="Invert Normals",
105 default=False
107 copy_seams = BoolProperty(
108 name="Copy Seams",
109 description="Copy Seams",
110 default=True
113 def execute(self, context):
114 props = context.scene.muv_props.transuv
115 active_obj = context.scene.objects.active
116 bm = bmesh.from_edit_mesh(active_obj.data)
117 if common.check_version(2, 73, 0) >= 0:
118 bm.faces.ensure_lookup_table()
120 # get UV layer
121 if not bm.loops.layers.uv:
122 self.report({'WARNING'}, "Object must have more than one UV map")
123 return {'CANCELLED'}
124 uv_layer = bm.loops.layers.uv.verify()
126 # get selection history
127 all_sel_faces = [
128 e for e in bm.select_history
129 if isinstance(e, bmesh.types.BMFace) and e.select]
130 if len(all_sel_faces) % 2 != 0:
131 self.report({'WARNING'}, "Two faces must be selected")
132 return {'CANCELLED'}
134 # parse selection history
135 for i, _ in enumerate(all_sel_faces):
136 if (i == 0) or (i % 2 == 0):
137 continue
138 sel_faces = [all_sel_faces[i - 1], all_sel_faces[i]]
139 active_face = all_sel_faces[i]
141 # parse all faces according to selection history
142 active_face_nor = active_face.normal.copy()
143 if self.invert_normals:
144 active_face_nor.negate()
145 all_sorted_faces = main_parse(
146 self, uv_layer, sel_faces, active_face,
147 active_face_nor)
149 if all_sorted_faces:
150 # check amount of copied/pasted faces
151 if len(all_sorted_faces) != len(props.topology_copied):
152 self.report(
153 {'WARNING'},
154 "Mesh has different amount of faces"
156 return {'FINISHED'}
158 for j, face_data in enumerate(all_sorted_faces.values()):
159 copied_data = props.topology_copied[j]
161 # check amount of copied/pasted verts
162 if len(copied_data[0]) != len(face_data[2]):
163 bpy.ops.mesh.select_all(action='DESELECT')
164 # select problematic face
165 list(all_sorted_faces.keys())[j].select = True
166 self.report(
167 {'WARNING'},
168 "Face have different amount of vertices"
170 return {'FINISHED'}
172 for k, (edge, uvloop) in enumerate(zip(face_data[1],
173 face_data[2])):
174 uvloop.uv = copied_data[0][k]
175 uvloop.pin_uv = copied_data[1][k]
176 if self.copy_seams:
177 edge.seam = copied_data[2][k]
179 bmesh.update_edit_mesh(active_obj.data)
180 if self.copy_seams:
181 active_obj.data.show_edge_seams = True
183 return {'FINISHED'}
186 def main_parse(
187 self, uv_layer, sel_faces,
188 active_face, active_face_nor):
189 all_sorted_faces = OrderedDict() # This is the main stuff
191 used_verts = set()
192 used_edges = set()
194 faces_to_parse = []
196 # get shared edge of two faces
197 cross_edges = []
198 for edge in active_face.edges:
199 if edge in sel_faces[0].edges and edge in sel_faces[1].edges:
200 cross_edges.append(edge)
202 # parse two selected faces
203 if cross_edges and len(cross_edges) == 1:
204 shared_edge = cross_edges[0]
205 vert1 = None
206 vert2 = None
208 dot_n = active_face_nor.normalized()
209 edge_vec_1 = (shared_edge.verts[1].co - shared_edge.verts[0].co)
210 edge_vec_len = edge_vec_1.length
211 edge_vec_1 = edge_vec_1.normalized()
213 af_center = active_face.calc_center_median()
214 af_vec = shared_edge.verts[0].co + (edge_vec_1 * (edge_vec_len * 0.5))
215 af_vec = (af_vec - af_center).normalized()
217 if af_vec.cross(edge_vec_1).dot(dot_n) > 0:
218 vert1 = shared_edge.verts[0]
219 vert2 = shared_edge.verts[1]
220 else:
221 vert1 = shared_edge.verts[1]
222 vert2 = shared_edge.verts[0]
224 # get active face stuff and uvs
225 face_stuff = get_other_verts_edges(
226 active_face, vert1, vert2, shared_edge, uv_layer)
227 all_sorted_faces[active_face] = face_stuff
228 used_verts.update(active_face.verts)
229 used_edges.update(active_face.edges)
231 # get first selected face stuff and uvs as they share shared_edge
232 second_face = sel_faces[0]
233 if second_face is active_face:
234 second_face = sel_faces[1]
235 face_stuff = get_other_verts_edges(
236 second_face, vert1, vert2, shared_edge, uv_layer)
237 all_sorted_faces[second_face] = face_stuff
238 used_verts.update(second_face.verts)
239 used_edges.update(second_face.edges)
241 # first Grow
242 faces_to_parse.append(active_face)
243 faces_to_parse.append(second_face)
245 else:
246 self.report({'WARNING'}, "Two faces should share one edge")
247 return None
249 # parse all faces
250 while True:
251 new_parsed_faces = []
252 if not faces_to_parse:
253 break
254 for face in faces_to_parse:
255 face_stuff = all_sorted_faces.get(face)
256 new_faces = parse_faces(
257 face, face_stuff, used_verts, used_edges, all_sorted_faces,
258 uv_layer)
259 if new_faces == 'CANCELLED':
260 self.report({'WARNING'}, "More than 2 faces share edge")
261 return None
263 new_parsed_faces += new_faces
264 faces_to_parse = new_parsed_faces
266 return all_sorted_faces
269 def parse_faces(
270 check_face, face_stuff, used_verts, used_edges, all_sorted_faces,
271 uv_layer):
272 """recurse faces around the new_grow only"""
274 new_shared_faces = []
275 for sorted_edge in face_stuff[1]:
276 shared_faces = sorted_edge.link_faces
277 if shared_faces:
278 if len(shared_faces) > 2:
279 bpy.ops.mesh.select_all(action='DESELECT')
280 for face_sel in shared_faces:
281 face_sel.select = True
282 shared_faces = []
283 return 'CANCELLED'
285 clear_shared_faces = get_new_shared_faces(
286 check_face, sorted_edge, shared_faces, all_sorted_faces.keys())
287 if clear_shared_faces:
288 shared_face = clear_shared_faces[0]
289 # get vertices of the edge
290 vert1 = sorted_edge.verts[0]
291 vert2 = sorted_edge.verts[1]
293 common.debug_print(face_stuff[0], vert1, vert2)
294 if face_stuff[0].index(vert1) > face_stuff[0].index(vert2):
295 vert1 = sorted_edge.verts[1]
296 vert2 = sorted_edge.verts[0]
298 common.debug_print(shared_face.verts, vert1, vert2)
299 new_face_stuff = get_other_verts_edges(
300 shared_face, vert1, vert2, sorted_edge, uv_layer)
301 all_sorted_faces[shared_face] = new_face_stuff
302 used_verts.update(shared_face.verts)
303 used_edges.update(shared_face.edges)
305 if common.DEBUG:
306 shared_face.select = True # test which faces are parsed
308 new_shared_faces.append(shared_face)
310 return new_shared_faces
313 def get_new_shared_faces(orig_face, shared_edge, check_faces, used_faces):
314 shared_faces = []
316 for face in check_faces:
317 is_shared_edge = shared_edge in face.edges
318 not_used = face not in used_faces
319 not_orig = face is not orig_face
320 not_hide = face.hide is False
321 if is_shared_edge and not_used and not_orig and not_hide:
322 shared_faces.append(face)
324 return shared_faces
327 def get_other_verts_edges(face, vert1, vert2, first_edge, uv_layer):
328 face_edges = [first_edge]
329 face_verts = [vert1, vert2]
330 face_loops = []
332 other_edges = [edge for edge in face.edges if edge not in face_edges]
334 for _ in range(len(other_edges)):
335 found_edge = None
336 # get sorted verts and edges
337 for edge in other_edges:
338 if face_verts[-1] in edge.verts:
339 other_vert = edge.other_vert(face_verts[-1])
341 if other_vert not in face_verts:
342 face_verts.append(other_vert)
344 found_edge = edge
345 if found_edge not in face_edges:
346 face_edges.append(edge)
347 break
349 other_edges.remove(found_edge)
351 # get sorted uvs
352 for vert in face_verts:
353 for loop in face.loops:
354 if loop.vert is vert:
355 face_loops.append(loop[uv_layer])
356 break
358 return [face_verts, face_edges, face_loops]