1 # SPDX-FileCopyrightText: 2017-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 __author__
= "Nutti <nutti.metro@gmail.com>, Jace Priester"
6 __status__
= "production"
8 __date__
= "22 Apr 2022"
12 from bpy
.props
import (
20 from ..utils
.bl_class_registry
import BlClassRegistry
21 from ..utils
.property_class_registry
import PropertyClassRegistry
22 from ..utils
import compatibility
as compat
25 def _is_valid_context(context
):
26 # only 'VIEW_3D' space is allowed to execute
27 if not common
.is_valid_space(context
, ['VIEW_3D']):
30 # Multiple objects editing mode is not supported in this feature.
31 objs
= common
.get_uv_editable_objects(context
)
35 # only edit mode is allowed to execute
36 if context
.object.mode
!= 'EDIT':
42 def get_copy_uv_layers(ops_obj
, bm
, uv_map
):
44 if uv_map
== "__default":
45 if not bm
.loops
.layers
.uv
:
47 {'WARNING'}, "Object must have more than one UV map")
49 uv_layers
.append(bm
.loops
.layers
.uv
.verify())
50 ops_obj
.report({'INFO'}, "Copy UV coordinate")
51 elif uv_map
== "__all":
52 for uv
in bm
.loops
.layers
.uv
.keys():
53 uv_layers
.append(bm
.loops
.layers
.uv
[uv
])
54 ops_obj
.report({'INFO'}, "Copy UV coordinate (UV map: ALL)")
56 uv_layers
.append(bm
.loops
.layers
.uv
[uv_map
])
58 {'INFO'}, "Copy UV coordinate (UV map: {})".format(uv_map
))
63 def get_paste_uv_layers(ops_obj
, obj
, bm
, src_info
, uv_map
):
65 if uv_map
== "__default":
66 if not bm
.loops
.layers
.uv
:
68 {'WARNING'}, "Object must have more than one UV map")
70 uv_layers
.append(bm
.loops
.layers
.uv
.verify())
71 ops_obj
.report({'INFO'}, "Paste UV coordinate")
72 elif uv_map
== "__new":
73 new_uv_map
= common
.create_new_uv_map(obj
)
75 ops_obj
.report({'WARNING'},
76 "Reached to the maximum number of UV map")
78 uv_layers
.append(bm
.loops
.layers
.uv
[new_uv_map
.name
])
81 "Paste UV coordinate (UV map: {})".format(new_uv_map
.name
))
82 elif uv_map
== "__all":
83 for src_layer
in src_info
.keys():
84 if src_layer
not in bm
.loops
.layers
.uv
.keys():
85 new_uv_map
= common
.create_new_uv_map(obj
, src_layer
)
87 ops_obj
.report({'WARNING'},
88 "Reached to the maximum number of UV map")
90 uv_layers
.append(bm
.loops
.layers
.uv
[src_layer
])
91 ops_obj
.report({'INFO'}, "Paste UV coordinate (UV map: ALL)")
93 uv_layers
.append(bm
.loops
.layers
.uv
[uv_map
])
95 {'INFO'}, "Paste UV coordinate (UV map: {})".format(uv_map
))
100 def get_src_face_info(ops_obj
, bm
, uv_layers
, only_select
=False):
102 for layer
in uv_layers
:
104 for face
in bm
.faces
:
105 if not only_select
or face
.select
:
108 "uvs": [l
[layer
].uv
.copy() for l
in face
.loops
],
109 "pin_uvs": [l
[layer
].pin_uv
for l
in face
.loops
],
110 "seams": [l
.edge
.seam
for l
in face
.loops
],
112 face_info
.append(info
)
114 ops_obj
.report({'WARNING'}, "No faces are selected")
116 src_info
[layer
.name
] = face_info
121 def get_dest_face_info(ops_obj
, bm
, uv_layers
, src_info
, strategy
,
124 for layer
in uv_layers
:
126 for face
in bm
.faces
:
127 if not only_select
or face
.select
:
130 "uvs": [l
[layer
].uv
.copy() for l
in face
.loops
],
132 face_info
.append(info
)
134 ops_obj
.report({'WARNING'}, "No faces are selected")
136 key
= list(src_info
.keys())[0]
137 src_face_count
= len(src_info
[key
])
138 dest_face_count
= len(face_info
)
139 if strategy
== 'N_N' and src_face_count
!= dest_face_count
:
142 "Number of selected faces is different from copied" +
144 .format(src_face_count
, dest_face_count
))
146 dest_info
[layer
.name
] = face_info
151 def _get_select_history_src_face_info(ops_obj
, bm
, uv_layers
):
153 for layer
in uv_layers
:
155 for hist
in bm
.select_history
:
156 if isinstance(hist
, bmesh
.types
.BMFace
) and hist
.select
:
159 "uvs": [l
[layer
].uv
.copy() for l
in hist
.loops
],
160 "pin_uvs": [l
[layer
].pin_uv
for l
in hist
.loops
],
161 "seams": [l
.edge
.seam
for l
in hist
.loops
],
163 face_info
.append(info
)
165 ops_obj
.report({'WARNING'}, "No faces are selected")
167 src_info
[layer
.name
] = face_info
172 def _get_select_history_dest_face_info(ops_obj
, bm
, uv_layers
, src_info
,
175 for layer
in uv_layers
:
177 for hist
in bm
.select_history
:
178 if isinstance(hist
, bmesh
.types
.BMFace
) and hist
.select
:
181 "uvs": [l
[layer
].uv
.copy() for l
in hist
.loops
],
183 face_info
.append(info
)
185 ops_obj
.report({'WARNING'}, "No faces are selected")
187 key
= list(src_info
.keys())[0]
188 src_face_count
= len(src_info
[key
])
189 dest_face_count
= len(face_info
)
190 if strategy
== 'N_N' and src_face_count
!= dest_face_count
:
193 "Number of selected faces is different from copied" +
195 .format(src_face_count
, dest_face_count
))
197 dest_info
[layer
.name
] = face_info
202 def paste_uv(ops_obj
, bm
, src_info
, dest_info
, uv_layers
, strategy
, flip
,
204 for slayer_name
, dlayer
in zip(src_info
.keys(), uv_layers
):
205 src_faces
= src_info
[slayer_name
]
206 dest_faces
= dest_info
[dlayer
.name
]
208 for idx
, dinfo
in enumerate(dest_faces
):
210 if strategy
== 'N_N':
211 sinfo
= src_faces
[idx
]
212 elif strategy
== 'N_M':
213 sinfo
= src_faces
[idx
% len(src_faces
)]
216 spuv
= sinfo
["pin_uvs"]
218 if len(sinfo
["uvs"]) != len(dinfo
["uvs"]):
219 ops_obj
.report({'WARNING'}, "Some faces are different size")
222 suvs_fr
= [uv
for uv
in suv
]
223 spuvs_fr
= [pin_uv
for pin_uv
in spuv
]
224 ss_fr
= [s
for s
in ss
]
233 for _
in range(rotate
):
235 pin_uv
= spuvs_fr
.pop()
237 suvs_fr
.insert(0, uv
)
238 spuvs_fr
.insert(0, pin_uv
)
242 for l
, suv
, spuv
, ss
in zip(bm
.faces
[dinfo
["index"]].loops
,
243 suvs_fr
, spuvs_fr
, ss_fr
):
245 l
[dlayer
].pin_uv
= spuv
246 if copy_seams
is True:
252 @PropertyClassRegistry()
254 idname
= "copy_paste_uv"
257 def init_props(cls
, scene
):
261 scene
.muv_props
.copy_paste_uv
= Props()
262 scene
.muv_props
.copy_paste_uv_selseq
= Props()
264 scene
.muv_copy_paste_uv_enabled
= BoolProperty(
265 name
="Copy/Paste UV Enabled",
266 description
="Copy/Paste UV is enabled",
269 scene
.muv_copy_paste_uv_copy_seams
= BoolProperty(
271 description
="Copy Seams",
274 scene
.muv_copy_paste_uv_mode
= EnumProperty(
276 ('DEFAULT', "Default", "Default Mode"),
277 ('SEL_SEQ', "Selection Sequence", "Selection Sequence Mode")
279 name
="Copy/Paste UV Mode",
280 description
="Copy/Paste UV Mode",
283 scene
.muv_copy_paste_uv_strategy
= EnumProperty(
285 description
="Paste Strategy",
287 ('N_N', 'N:N', 'Number of faces must be equal to source'),
288 ('N_M', 'N:M', 'Number of faces must not be equal to source')
294 def del_props(cls
, scene
):
295 del scene
.muv_props
.copy_paste_uv
296 del scene
.muv_props
.copy_paste_uv_selseq
297 del scene
.muv_copy_paste_uv_enabled
298 del scene
.muv_copy_paste_uv_copy_seams
299 del scene
.muv_copy_paste_uv_mode
300 del scene
.muv_copy_paste_uv_strategy
304 @compat.make_annotations
305 class MUV_OT_CopyPasteUV_CopyUV(bpy
.types
.Operator
):
307 Operation class: Copy UV coordinate
310 bl_idname
= "uv.muv_copy_paste_uv_copy_uv"
312 bl_description
= "Copy UV coordinate"
313 bl_options
= {'REGISTER', 'UNDO'}
315 uv_map
= StringProperty(default
="__default", options
={'HIDDEN'})
318 def poll(cls
, context
):
319 # we can not get area/space/region from console
320 if common
.is_console_mode():
322 return _is_valid_context(context
)
324 def execute(self
, context
):
325 props
= context
.scene
.muv_props
.copy_paste_uv
327 objs
= common
.get_uv_editable_objects(context
)
328 # poll() method ensures that only one object is selected.
330 bm
= common
.create_bmesh(obj
)
333 uv_layers
= get_copy_uv_layers(self
, bm
, self
.uv_map
)
338 src_info
= get_src_face_info(self
, bm
, uv_layers
, True)
341 props
.src_info
= src_info
343 face_count
= len(props
.src_info
[list(props
.src_info
.keys())[0]])
344 self
.report({'INFO'}, "{} face(s) are copied".format(face_count
))
350 class MUV_MT_CopyPasteUV_CopyUV(bpy
.types
.Menu
):
352 Menu class: Copy UV coordinate
355 bl_idname
= "MUV_MT_CopyPasteUV_CopyUV"
356 bl_label
= "Copy UV (Menu)"
357 bl_description
= "Menu of Copy UV coordinate"
360 def poll(cls
, context
):
361 return _is_valid_context(context
)
363 def draw(self
, context
):
365 objs
= common
.get_uv_editable_objects(context
)
366 # poll() method ensures that only one object is selected.
370 bm
= common
.create_bmesh(obj
)
371 uv_maps
= bm
.loops
.layers
.uv
.keys()
373 ops
= layout
.operator(MUV_OT_CopyPasteUV_CopyUV
.bl_idname
,
375 ops
.uv_map
= "__default"
377 ops
= layout
.operator(MUV_OT_CopyPasteUV_CopyUV
.bl_idname
,
382 ops
= layout
.operator(MUV_OT_CopyPasteUV_CopyUV
.bl_idname
, text
=m
)
387 @compat.make_annotations
388 class MUV_OT_CopyPasteUV_PasteUV(bpy
.types
.Operator
):
390 Operation class: Paste UV coordinate
393 bl_idname
= "uv.muv_copy_paste_uv_paste_uv"
394 bl_label
= "Paste UV"
395 bl_description
= "Paste UV coordinate"
396 bl_options
= {'REGISTER', 'UNDO'}
398 uv_map
= StringProperty(default
="__default", options
={'HIDDEN'})
399 strategy
= EnumProperty(
401 description
="Paste Strategy",
403 ('N_N', 'N:N', 'Number of faces must be equal to source'),
404 ('N_M', 'N:M', 'Number of faces must not be equal to source')
408 flip_copied_uv
= BoolProperty(
409 name
="Flip Copied UV",
410 description
="Flip Copied UV...",
413 rotate_copied_uv
= IntProperty(
415 name
="Rotate Copied UV",
419 copy_seams
= BoolProperty(
421 description
="Copy Seams",
426 def poll(cls
, context
):
427 # we can not get area/space/region from console
428 if common
.is_console_mode():
431 props
= sc
.muv_props
.copy_paste_uv
432 if not props
.src_info
:
434 return _is_valid_context(context
)
436 def execute(self
, context
):
437 props
= context
.scene
.muv_props
.copy_paste_uv
438 if not props
.src_info
:
439 self
.report({'WARNING'}, "Need copy UV at first")
442 objs
= common
.get_uv_editable_objects(context
)
443 # poll() method ensures that only one object is selected.
445 bm
= common
.create_bmesh(obj
)
448 uv_layers
= get_paste_uv_layers(self
, obj
, bm
, props
.src_info
,
454 dest_info
= get_dest_face_info(self
, bm
, uv_layers
,
455 props
.src_info
, self
.strategy
, True)
456 if dest_info
is None:
460 ret
= paste_uv(self
, bm
, props
.src_info
, dest_info
, uv_layers
,
461 self
.strategy
, self
.flip_copied_uv
,
462 self
.rotate_copied_uv
, self
.copy_seams
)
466 face_count
= len(props
.src_info
[list(dest_info
.keys())[0]])
467 self
.report({'INFO'}, "{} face(s) are pasted".format(face_count
))
469 bmesh
.update_edit_mesh(obj
.data
)
471 if compat
.check_version(2, 80, 0) < 0:
472 if self
.copy_seams
is True:
473 obj
.data
.show_edge_seams
= True
479 class MUV_MT_CopyPasteUV_PasteUV(bpy
.types
.Menu
):
481 Menu class: Paste UV coordinate
484 bl_idname
= "MUV_MT_CopyPasteUV_PasteUV"
485 bl_label
= "Paste UV (Menu)"
486 bl_description
= "Menu of Paste UV coordinate"
489 def poll(cls
, context
):
491 props
= sc
.muv_props
.copy_paste_uv
492 if not props
.src_info
:
494 return _is_valid_context(context
)
496 def draw(self
, context
):
499 objs
= common
.get_uv_editable_objects(context
)
500 # poll() method ensures that only one object is selected.
504 bm
= common
.create_bmesh(obj
)
505 uv_maps
= bm
.loops
.layers
.uv
.keys()
507 ops
= layout
.operator(MUV_OT_CopyPasteUV_PasteUV
.bl_idname
,
509 ops
.uv_map
= "__default"
510 ops
.copy_seams
= sc
.muv_copy_paste_uv_copy_seams
511 ops
.strategy
= sc
.muv_copy_paste_uv_strategy
513 ops
= layout
.operator(MUV_OT_CopyPasteUV_PasteUV
.bl_idname
,
516 ops
.copy_seams
= sc
.muv_copy_paste_uv_copy_seams
517 ops
.strategy
= sc
.muv_copy_paste_uv_strategy
519 ops
= layout
.operator(MUV_OT_CopyPasteUV_PasteUV
.bl_idname
,
522 ops
.copy_seams
= sc
.muv_copy_paste_uv_copy_seams
523 ops
.strategy
= sc
.muv_copy_paste_uv_strategy
526 ops
= layout
.operator(MUV_OT_CopyPasteUV_PasteUV
.bl_idname
, text
=m
)
528 ops
.copy_seams
= sc
.muv_copy_paste_uv_copy_seams
529 ops
.strategy
= sc
.muv_copy_paste_uv_strategy
533 @compat.make_annotations
534 class MUV_OT_CopyPasteUV_SelSeqCopyUV(bpy
.types
.Operator
):
536 Operation class: Copy UV coordinate by selection sequence
539 bl_idname
= "uv.muv_copy_paste_uv_selseq_copy_uv"
540 bl_label
= "Copy UV (Selection Sequence)"
541 bl_description
= "Copy UV data by selection sequence"
542 bl_options
= {'REGISTER', 'UNDO'}
544 uv_map
= StringProperty(default
="__default", options
={'HIDDEN'})
547 def poll(cls
, context
):
548 # we can not get area/space/region from console
549 if common
.is_console_mode():
551 return _is_valid_context(context
)
553 def execute(self
, context
):
554 props
= context
.scene
.muv_props
.copy_paste_uv_selseq
556 objs
= common
.get_uv_editable_objects(context
)
557 # poll() method ensures that only one object is selected.
559 bm
= common
.create_bmesh(obj
)
562 uv_layers
= get_copy_uv_layers(self
, bm
, self
.uv_map
)
567 src_info
= _get_select_history_src_face_info(self
, bm
, uv_layers
)
570 props
.src_info
= src_info
572 face_count
= len(props
.src_info
[list(props
.src_info
.keys())[0]])
573 self
.report({'INFO'}, "{} face(s) are selected".format(face_count
))
579 class MUV_MT_CopyPasteUV_SelSeqCopyUV(bpy
.types
.Menu
):
581 Menu class: Copy UV coordinate by selection sequence
584 bl_idname
= "MUV_MT_CopyPasteUV_SelSeqCopyUV"
585 bl_label
= "Copy UV (Selection Sequence) (Menu)"
586 bl_description
= "Menu of Copy UV coordinate by selection sequence"
589 def poll(cls
, context
):
590 return _is_valid_context(context
)
592 def draw(self
, context
):
594 objs
= common
.get_uv_editable_objects(context
)
595 # poll() method ensures that only one object is selected.
598 bm
= common
.create_bmesh(obj
)
599 uv_maps
= bm
.loops
.layers
.uv
.keys()
601 ops
= layout
.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV
.bl_idname
,
603 ops
.uv_map
= "__default"
605 ops
= layout
.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV
.bl_idname
,
610 ops
= layout
.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV
.bl_idname
,
616 @compat.make_annotations
617 class MUV_OT_CopyPasteUV_SelSeqPasteUV(bpy
.types
.Operator
):
619 Operation class: Paste UV coordinate by selection sequence
622 bl_idname
= "uv.muv_copy_paste_uv_selseq_paste_uv"
623 bl_label
= "Paste UV (Selection Sequence)"
624 bl_description
= "Paste UV coordinate by selection sequence"
625 bl_options
= {'REGISTER', 'UNDO'}
627 uv_map
= StringProperty(default
="__default", options
={'HIDDEN'})
628 strategy
= EnumProperty(
630 description
="Paste Strategy",
632 ('N_N', 'N:N', 'Number of faces must be equal to source'),
633 ('N_M', 'N:M', 'Number of faces must not be equal to source')
637 flip_copied_uv
= BoolProperty(
638 name
="Flip Copied UV",
639 description
="Flip Copied UV...",
642 rotate_copied_uv
= IntProperty(
644 name
="Rotate Copied UV",
648 copy_seams
= BoolProperty(
650 description
="Copy Seams",
655 def poll(cls
, context
):
656 # we can not get area/space/region from console
657 if common
.is_console_mode():
660 props
= sc
.muv_props
.copy_paste_uv_selseq
661 if not props
.src_info
:
663 return _is_valid_context(context
)
665 def execute(self
, context
):
666 props
= context
.scene
.muv_props
.copy_paste_uv_selseq
667 if not props
.src_info
:
668 self
.report({'WARNING'}, "Need copy UV at first")
671 objs
= common
.get_uv_editable_objects(context
)
672 # poll() method ensures that only one object is selected.
674 bm
= common
.create_bmesh(obj
)
677 uv_layers
= get_paste_uv_layers(self
, obj
, bm
, props
.src_info
,
683 dest_info
= _get_select_history_dest_face_info(self
, bm
, uv_layers
,
686 if dest_info
is None:
690 ret
= paste_uv(self
, bm
, props
.src_info
, dest_info
, uv_layers
,
691 self
.strategy
, self
.flip_copied_uv
,
692 self
.rotate_copied_uv
, self
.copy_seams
)
696 face_count
= len(props
.src_info
[list(dest_info
.keys())[0]])
697 self
.report({'INFO'}, "{} face(s) are pasted".format(face_count
))
699 bmesh
.update_edit_mesh(obj
.data
)
701 if compat
.check_version(2, 80, 0) < 0:
702 if self
.copy_seams
is True:
703 obj
.data
.show_edge_seams
= True
709 class MUV_MT_CopyPasteUV_SelSeqPasteUV(bpy
.types
.Menu
):
711 Menu class: Paste UV coordinate by selection sequence
714 bl_idname
= "MUV_MT_CopyPasteUV_SelSeqPasteUV"
715 bl_label
= "Paste UV (Selection Sequence) (Menu)"
716 bl_description
= "Menu of Paste UV coordinate by selection sequence"
719 def poll(cls
, context
):
721 props
= sc
.muv_props
.copy_paste_uv_selseq
722 if not props
.src_info
:
724 return _is_valid_context(context
)
726 def draw(self
, context
):
729 objs
= common
.get_uv_editable_objects(context
)
730 # poll() method ensures that only one object is selected.
734 bm
= common
.create_bmesh(obj
)
735 uv_maps
= bm
.loops
.layers
.uv
.keys()
737 ops
= layout
.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV
.bl_idname
,
739 ops
.uv_map
= "__default"
740 ops
.copy_seams
= sc
.muv_copy_paste_uv_copy_seams
741 ops
.strategy
= sc
.muv_copy_paste_uv_strategy
743 ops
= layout
.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV
.bl_idname
,
746 ops
.copy_seams
= sc
.muv_copy_paste_uv_copy_seams
747 ops
.strategy
= sc
.muv_copy_paste_uv_strategy
749 ops
= layout
.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV
.bl_idname
,
752 ops
.copy_seams
= sc
.muv_copy_paste_uv_copy_seams
753 ops
.strategy
= sc
.muv_copy_paste_uv_strategy
756 ops
= layout
.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV
.bl_idname
,
759 ops
.copy_seams
= sc
.muv_copy_paste_uv_copy_seams
760 ops
.strategy
= sc
.muv_copy_paste_uv_strategy