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"
8 __date__
= "22 Apr 2022"
11 from math
import atan2
, sin
, cos
15 from mathutils
import Vector
16 from bpy
.props
import BoolProperty
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
29 if not common
.is_valid_space(context
, ['IMAGE_EDITOR', 'VIEW_3D']):
32 # Multiple objects editing mode is not supported in this feature.
33 objs
= common
.get_uv_editable_objects(context
)
37 # only edit mode is allowed to execute
38 if context
.object.mode
!= 'EDIT':
44 @PropertyClassRegistry()
46 idname
= "copy_paste_uv_uvedit"
49 def init_props(cls
, scene
):
50 class CopyPastUVProps():
53 class CopyPasteUVIslandProps():
57 # "uv_layer": UV Layer,
58 # "island": UV Island,
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(
69 description
="Paste to the target uniquely",
74 def del_props(cls
, scene
):
75 del scene
.muv_props
.copy_paste_uv_uvedit
76 del scene
.muv_props
.copy_paste_uv_island
80 class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy
.types
.Operator
):
82 Operation class: Copy UV coordinate on UV/Image Editor
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'}
91 def poll(cls
, context
):
92 # we can not get area/space/region from console
93 if common
.is_console_mode():
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.
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()
109 for face
in bm
.faces
:
114 if not l
[uv_layer
].select
:
119 props
.src_uvs
.append([l
[uv_layer
].uv
.copy() for l
in face
.loops
])
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'}
136 def poll(cls
, context
):
137 # we can not get area/space/region from console
138 if common
.is_console_mode():
141 props
= sc
.muv_props
.copy_paste_uv_uvedit
142 if not props
.src_uvs
:
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.
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()
158 dest_face_indices
= []
159 for face
in bm
.faces
:
164 if not l
[uv_layer
].select
:
169 dest_face_indices
.append(face
.index
)
170 uvs
= [l
[uv_layer
].uv
.copy() for l
in face
.loops
]
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]
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
189 ratio
= dest_diff
.length
/ src_diff
.length
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
)
198 turn
= Vector((length
* cos(radian_fin
),
199 length
* sin(radian_fin
)))
200 target_uv
= Vector((turn
.x
* ratio
, turn
.y
* ratio
)) + \
202 l
[uv_layer
].uv
= target_uv
204 bmesh
.update_edit_mesh(obj
.data
)
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
):
217 if context
.tool_settings
.use_uv_select_sync
:
218 for f
in island
["faces"]:
223 for f
in island
["faces"]:
224 for l
in f
["face"].loops
:
226 if l
[uv_layer
].select
:
229 return selected_count
, all_count
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'}
244 def poll(cls
, context
):
245 # we can not get area/space/region from console
246 if common
.is_console_mode():
248 return _is_valid_context(context
)
250 def execute(self
, context
):
252 props
= sc
.muv_props
.copy_paste_uv_island
255 props
.src_objects
= []
256 objs
= common
.get_uv_editable_objects(context
)
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)
269 islands
= common
.get_island_info_from_bmesh(
270 bm
, only_selected
=True)
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:
276 if selected_count
!= all_count
:
279 "All UVs belonging to the island must be selected")
284 "uv_layer": uv_layer
,
287 props
.src_data
.append(data
)
288 props
.src_objects
.append(obj
)
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",
312 def poll(cls
, context
):
313 # we can not get area/space/region from console
314 if common
.is_console_mode():
317 props
= sc
.muv_props
.copy_paste_uv_island
318 if not props
.src_data
:
320 return _is_valid_context(context
)
322 def execute(self
, context
):
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
= {}
331 bms_and_uv_layers
[d
["bmesh"]] = d
["uv_layer"]
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)
338 islands
= common
.get_island_info_from_bmesh(
339 bm
, only_selected
=True)
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:
345 if selected_count
!= all_count
:
348 "All UVs belonging to the island must be selected")
354 "uv_layer": uv_layer
,
360 for ddata
in dst_data
:
362 for f
in ddata
["island"]["faces"]:
363 for l
in f
["face"].loops
:
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
:
373 for f
in sdata
["island"]["faces"]:
374 for l
in f
["face"].loops
:
376 src_uv_layer
= sdata
["uv_layer"]
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
)
387 for n1
, n2
in pairs
.items():
388 uv1
= n1
.value
["uv_vert"][src_uv_layer
].uv
389 l2
= n2
.value
["loops"]
391 l
[dst_uv_layer
].uv
= uv1
395 self
.report({'WARNING'}, "Island does not match")
399 bmesh
.update_edit_mesh(obj
.data
)