Cleanup: Node Wrangler: preview_node operator
[blender-addons.git] / magic_uv / op / copy_paste_uv_uvedit.py
blob79bc8f0cc55579383dffbe93ca09b1ba5056ff9a
1 # SPDX-FileCopyrightText: 2018-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 __author__ = "imdjs, Nutti <nutti.metro@gmail.com>"
6 __status__ = "production"
7 __version__ = "6.6"
8 __date__ = "22 Apr 2022"
10 import math
11 from math import atan2, sin, cos
13 import bpy
14 import bmesh
15 from mathutils import Vector
16 from bpy.props import BoolProperty
18 from .. import common
19 from ..utils.bl_class_registry import BlClassRegistry
20 from ..utils.property_class_registry import PropertyClassRegistry
21 from ..utils.graph import graph_is_isomorphic
22 from ..utils import compatibility as compat
25 def _is_valid_context(context):
26 # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
27 # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
28 # after the execution
29 if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']):
30 return False
32 # Multiple objects editing mode is not supported in this feature.
33 objs = common.get_uv_editable_objects(context)
34 if len(objs) != 1:
35 return False
37 # only edit mode is allowed to execute
38 if context.object.mode != 'EDIT':
39 return False
41 return True
44 @PropertyClassRegistry()
45 class _Properties:
46 idname = "copy_paste_uv_uvedit"
48 @classmethod
49 def init_props(cls, scene):
50 class CopyPastUVProps():
51 src_uvs = None
53 class CopyPasteUVIslandProps():
54 # [
55 # {
56 # "bmesh": BMesh,
57 # "uv_layer": UV Layer,
58 # "island": UV Island,
59 # }
60 # ]
61 src_data = []
62 src_objects = []
64 scene.muv_props.copy_paste_uv_uvedit = CopyPastUVProps()
65 scene.muv_props.copy_paste_uv_island = CopyPasteUVIslandProps()
67 scene.muv_copy_paste_uv_uvedit_unique_target = BoolProperty(
68 name="Unique Target",
69 description="Paste to the target uniquely",
70 default=False
73 @classmethod
74 def del_props(cls, scene):
75 del scene.muv_props.copy_paste_uv_uvedit
76 del scene.muv_props.copy_paste_uv_island
79 @BlClassRegistry()
80 class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy.types.Operator):
81 """
82 Operation class: Copy UV coordinate on UV/Image Editor
83 """
85 bl_idname = "uv.muv_copy_paste_uv_uvedit_copy_uv"
86 bl_label = "Copy UV (UV/Image Editor)"
87 bl_description = "Copy UV coordinate (only selected in UV/Image Editor)"
88 bl_options = {'REGISTER', 'UNDO'}
90 @classmethod
91 def poll(cls, context):
92 # we can not get area/space/region from console
93 if common.is_console_mode():
94 return True
95 return _is_valid_context(context)
97 def execute(self, context):
98 props = context.scene.muv_props.copy_paste_uv_uvedit
100 objs = common.get_uv_editable_objects(context)
101 # poll() method ensures that only one object is selected.
102 obj = objs[0]
103 bm = bmesh.from_edit_mesh(obj.data)
104 uv_layer = bm.loops.layers.uv.verify()
105 if common.check_version(2, 73, 0) >= 0:
106 bm.faces.ensure_lookup_table()
108 props.src_uvs = []
109 for face in bm.faces:
110 if not face.select:
111 continue
112 skip = False
113 for l in face.loops:
114 if not l[uv_layer].select:
115 skip = True
116 break
117 if skip:
118 continue
119 props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops])
121 return {'FINISHED'}
124 @BlClassRegistry()
125 class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator):
127 Operation class: Paste UV coordinate on UV/Image Editor
130 bl_idname = "uv.muv_copy_paste_uv_uvedit_paste_uv"
131 bl_label = "Paste UV (UV/Image Editor)"
132 bl_description = "Paste UV coordinate (only selected in UV/Image Editor)"
133 bl_options = {'REGISTER', 'UNDO'}
135 @classmethod
136 def poll(cls, context):
137 # we can not get area/space/region from console
138 if common.is_console_mode():
139 return True
140 sc = context.scene
141 props = sc.muv_props.copy_paste_uv_uvedit
142 if not props.src_uvs:
143 return False
144 return _is_valid_context(context)
146 def execute(self, context):
147 props = context.scene.muv_props.copy_paste_uv_uvedit
149 objs = common.get_uv_editable_objects(context)
150 # poll() method ensures that only one object is selected.
151 obj = objs[0]
152 bm = bmesh.from_edit_mesh(obj.data)
153 uv_layer = bm.loops.layers.uv.verify()
154 if common.check_version(2, 73, 0) >= 0:
155 bm.faces.ensure_lookup_table()
157 dest_uvs = []
158 dest_face_indices = []
159 for face in bm.faces:
160 if not face.select:
161 continue
162 skip = False
163 for l in face.loops:
164 if not l[uv_layer].select:
165 skip = True
166 break
167 if skip:
168 continue
169 dest_face_indices.append(face.index)
170 uvs = [l[uv_layer].uv.copy() for l in face.loops]
171 dest_uvs.append(uvs)
173 for suvs, duvs in zip(props.src_uvs, dest_uvs):
174 src_diff = suvs[1] - suvs[0]
175 dest_diff = duvs[1] - duvs[0]
177 src_base = suvs[0]
178 dest_base = duvs[0]
180 src_rad = atan2(src_diff.y, src_diff.x)
181 dest_rad = atan2(dest_diff.y, dest_diff.x)
182 if src_rad < dest_rad:
183 radian = dest_rad - src_rad
184 elif src_rad > dest_rad:
185 radian = math.pi * 2 - (src_rad - dest_rad)
186 else: # src_rad == dest_rad
187 radian = 0.0
189 ratio = dest_diff.length / src_diff.length
190 break
192 for suvs, fidx in zip(props.src_uvs, dest_face_indices):
193 for l, suv in zip(bm.faces[fidx].loops, suvs):
194 base = suv - src_base
195 radian_ref = atan2(base.y, base.x)
196 radian_fin = (radian + radian_ref)
197 length = base.length
198 turn = Vector((length * cos(radian_fin),
199 length * sin(radian_fin)))
200 target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \
201 dest_base
202 l[uv_layer].uv = target_uv
204 bmesh.update_edit_mesh(obj.data)
206 return {'FINISHED'}
209 # Return selected/all count.
210 # If context.tool_settings.use_uv_select_sync is enabled:
211 # Return selected/all face count.
212 # If context.tool_settings.use_uv_select_sync is disabled:
213 # Return selected/all loop count.
214 def get_counts(context, island, uv_layer):
215 selected_count = 0
216 all_count = 0
217 if context.tool_settings.use_uv_select_sync:
218 for f in island["faces"]:
219 all_count += 1
220 if f["face"].select:
221 selected_count += 1
222 else:
223 for f in island["faces"]:
224 for l in f["face"].loops:
225 all_count += 1
226 if l[uv_layer].select:
227 selected_count += 1
229 return selected_count, all_count
232 @BlClassRegistry()
233 class MUV_OT_CopyPasteUVUVEdit_CopyUVIsland(bpy.types.Operator):
235 Operation class: Copy UV island on UV/Image Editor
238 bl_idname = "uv.muv_copy_paste_uv_uvedit_copy_uv_island"
239 bl_label = "Copy UV Island (UV/Image Editor)"
240 bl_description = "Copy UV island (only selected in UV/Image Editor)"
241 bl_options = {'REGISTER', 'UNDO'}
243 @classmethod
244 def poll(cls, context):
245 # we can not get area/space/region from console
246 if common.is_console_mode():
247 return True
248 return _is_valid_context(context)
250 def execute(self, context):
251 sc = context.scene
252 props = sc.muv_props.copy_paste_uv_island
254 props.src_data = []
255 props.src_objects = []
256 objs = common.get_uv_editable_objects(context)
257 for obj in objs:
258 bm = bmesh.from_edit_mesh(obj.data)
259 uv_layer = bm.loops.layers.uv.verify()
260 if common.check_version(2, 73, 0) >= 0:
261 bm.verts.ensure_lookup_table()
262 bm.edges.ensure_lookup_table()
263 bm.faces.ensure_lookup_table()
265 if context.tool_settings.use_uv_select_sync:
266 islands = common.get_island_info_from_bmesh(
267 bm, only_selected=False)
268 else:
269 islands = common.get_island_info_from_bmesh(
270 bm, only_selected=True)
271 for isl in islands:
272 # Check if all UVs belonging to the island is selected.
273 selected_count, all_count = get_counts(context, isl, uv_layer)
274 if selected_count == 0:
275 continue
276 if selected_count != all_count:
277 self.report(
278 {'WARNING'},
279 "All UVs belonging to the island must be selected")
280 return {'CANCELLED'}
282 data = {
283 "bmesh": bm,
284 "uv_layer": uv_layer,
285 "island": isl
287 props.src_data.append(data)
288 props.src_objects.append(obj)
290 return {'FINISHED'}
293 @BlClassRegistry()
294 @compat.make_annotations
295 class MUV_OT_CopyPasteUVUVEdit_PasteUVIsland(bpy.types.Operator):
297 Operation class: Paste UV island on UV/Image Editor
300 bl_idname = "uv.muv_copy_paste_uv_uvedit_paste_uv_island"
301 bl_label = "Paste UV Island (UV/Image Editor)"
302 bl_description = "Paste UV island (only selected in UV/Image Editor)"
303 bl_options = {'REGISTER', 'UNDO'}
305 unique_target = BoolProperty(
306 name="Unique Target",
307 description="Paste to the target uniquely",
308 default=False
311 @classmethod
312 def poll(cls, context):
313 # we can not get area/space/region from console
314 if common.is_console_mode():
315 return True
316 sc = context.scene
317 props = sc.muv_props.copy_paste_uv_island
318 if not props.src_data:
319 return False
320 return _is_valid_context(context)
322 def execute(self, context):
323 sc = context.scene
324 props = sc.muv_props.copy_paste_uv_island
326 src_data = props.src_data
327 src_objs = props.src_objects
329 bms_and_uv_layers = {}
330 for d in src_data:
331 bms_and_uv_layers[d["bmesh"]] = d["uv_layer"]
332 dst_data = []
333 for bm, uv_layer in bms_and_uv_layers.items():
334 if context.tool_settings.use_uv_select_sync:
335 islands = common.get_island_info_from_bmesh(
336 bm, only_selected=False)
337 else:
338 islands = common.get_island_info_from_bmesh(
339 bm, only_selected=True)
340 for isl in islands:
341 # Check if all UVs belonging to the island is selected.
342 selected_count, all_count = get_counts(context, isl, uv_layer)
343 if selected_count == 0:
344 continue
345 if selected_count != all_count:
346 self.report(
347 {'WARNING'},
348 "All UVs belonging to the island must be selected")
349 return {'CANCELLED'}
351 dst_data.append(
353 "bm": bm,
354 "uv_layer": uv_layer,
355 "island": isl,
359 used = []
360 for ddata in dst_data:
361 dst_loops = []
362 for f in ddata["island"]["faces"]:
363 for l in f["face"].loops:
364 dst_loops.append(l)
365 dst_uv_layer = ddata["uv_layer"]
367 # Find a suitable island.
368 for sdata in src_data:
369 if self.unique_target and sdata in used:
370 continue
372 src_loops = []
373 for f in sdata["island"]["faces"]:
374 for l in f["face"].loops:
375 src_loops.append(l)
376 src_uv_layer = sdata["uv_layer"]
378 # Create UV graph.
379 src_uv_graph = common.create_uv_graph(src_loops, src_uv_layer)
380 dst_uv_graph = common.create_uv_graph(dst_loops, dst_uv_layer)
382 # Check if the graph is isomorphic.
383 # If the graph is isomorphic, matching pair is returned.
384 result, pairs = graph_is_isomorphic(src_uv_graph, dst_uv_graph)
385 if result:
386 # Paste UV island.
387 for n1, n2 in pairs.items():
388 uv1 = n1.value["uv_vert"][src_uv_layer].uv
389 l2 = n2.value["loops"]
390 for l in l2:
391 l[dst_uv_layer].uv = uv1
392 used.append(sdata)
393 break
394 else:
395 self.report({'WARNING'}, "Island does not match")
396 return {'CANCELLED'}
398 for obj in src_objs:
399 bmesh.update_edit_mesh(obj.data)
401 return {'FINISHED'}