Node Wrangler: create world output if the node tree is of type world
[blender-addons.git] / magic_uv / op / align_uv.py
blob18255045cb5165d3182fb60356c6cb475c74eda3
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 __author__ = "imdjs, Nutti <nutti.metro@gmail.com>"
4 __status__ = "production"
5 __version__ = "6.6"
6 __date__ = "22 Apr 2022"
8 import math
9 from math import atan2, tan, sin, cos
11 import bpy
12 from bpy.props import (
13 EnumProperty,
14 BoolProperty,
15 FloatProperty,
16 FloatVectorProperty,
18 import bmesh
19 from mathutils import Vector
21 from ..utils.bl_class_registry import BlClassRegistry
22 from ..utils.property_class_registry import PropertyClassRegistry
23 from ..utils import compatibility as compat
25 from .. import common
28 def _is_valid_context(context):
29 # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
30 # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
31 # after the execution
32 if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']):
33 return False
35 objs = common.get_uv_editable_objects(context)
36 if not objs:
37 return False
39 # only edit mode is allowed to execute
40 if context.object.mode != 'EDIT':
41 return False
43 return True
46 # get sum vertex length of loop sequences
47 def _get_loop_vert_len(loops):
48 length = 0
49 for l1, l2 in zip(loops[:-1], loops[1:]):
50 diff = l2.vert.co - l1.vert.co
51 length = length + abs(diff.length)
53 return length
56 # get sum uv length of loop sequences
57 def _get_loop_uv_len(loops, uv_layer):
58 length = 0
59 for l1, l2 in zip(loops[:-1], loops[1:]):
60 diff = l2[uv_layer].uv - l1[uv_layer].uv
61 length = length + abs(diff.length)
63 return length
66 # get center/radius of circle by 3 vertices
67 def _get_circle(v):
68 alpha = atan2((v[0].y - v[1].y), (v[0].x - v[1].x)) + math.pi / 2
69 beta = atan2((v[1].y - v[2].y), (v[1].x - v[2].x)) + math.pi / 2
70 ex = (v[0].x + v[1].x) / 2.0
71 ey = (v[0].y + v[1].y) / 2.0
72 fx = (v[1].x + v[2].x) / 2.0
73 fy = (v[1].y + v[2].y) / 2.0
74 cx = (ey - fy - ex * tan(alpha) + fx * tan(beta)) / \
75 (tan(beta) - tan(alpha))
76 cy = ey - (ex - cx) * tan(alpha)
77 center = Vector((cx, cy))
79 r = v[0] - center
80 radian = r.length
82 return center, radian
85 # get position on circle with same arc length
86 def _calc_v_on_circle(v, center, radius):
87 base = v[0]
88 theta = atan2(base.y - center.y, base.x - center.x)
89 new_v = []
90 for i in range(len(v)):
91 angle = theta + i * 2 * math.pi / len(v)
92 new_v.append(Vector((center.x + radius * sin(angle),
93 center.y + radius * cos(angle))))
95 return new_v
98 # get accumulate vertex lengths of loop sequences
99 def _get_loop_vert_accum_len(loops):
100 accum_lengths = [0.0]
101 length = 0
102 for l1, l2 in zip(loops[:-1], loops[1:]):
103 diff = l2.vert.co - l1.vert.co
104 length = length + abs(diff.length)
105 accum_lengths.extend([length])
107 return accum_lengths
110 # get sum uv length of loop sequences
111 def _get_loop_uv_accum_len(loops, uv_layer):
112 accum_lengths = [0.0]
113 length = 0
114 for l1, l2 in zip(loops[:-1], loops[1:]):
115 diff = l2[uv_layer].uv - l1[uv_layer].uv
116 length = length + abs(diff.length)
117 accum_lengths.extend([length])
119 return accum_lengths
122 # get horizontal differential of UV influenced by mesh vertex
123 def _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl):
124 common.debug_print(
125 "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx))
127 base_uv = loop_seqs[0][vidx][0][uv_layer].uv.copy()
129 # calculate original length
130 hloops = []
131 for s in loop_seqs:
132 hloops.extend([s[vidx][0], s[vidx][1]])
133 total_vlen = _get_loop_vert_len(hloops)
134 accum_vlens = _get_loop_vert_accum_len(hloops)
135 total_uvlen = _get_loop_uv_len(hloops, uv_layer)
136 accum_uvlens = _get_loop_uv_accum_len(hloops, uv_layer)
137 orig_uvs = [l[uv_layer].uv.copy() for l in hloops]
139 # calculate target length
140 tgt_noinfl = total_uvlen * (hidx + pidx) / len(loop_seqs)
141 tgt_infl = total_uvlen * accum_vlens[hidx * 2 + pidx] / total_vlen
142 target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl
143 common.debug_print(target_length)
144 common.debug_print(accum_uvlens)
146 # calculate target UV
147 for i in range(len(accum_uvlens[:-1])):
148 # get line segment which UV will be placed
149 if accum_uvlens[i] <= target_length < accum_uvlens[i + 1]:
150 tgt_seg_len = target_length - accum_uvlens[i]
151 seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
152 uv1 = orig_uvs[i]
153 uv2 = orig_uvs[i + 1]
154 target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
155 break
156 elif i == (len(accum_uvlens[:-1]) - 1):
157 if abs(accum_uvlens[i + 1] - target_length) > 0.000001:
158 raise Exception(
159 "Internal Error: horizontal_target_length={}"
160 " is not equal to {}"
161 .format(target_length, accum_uvlens[-1]))
162 tgt_seg_len = target_length - accum_uvlens[i]
163 seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
164 uv1 = orig_uvs[i]
165 uv2 = orig_uvs[i + 1]
166 target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
167 break
168 else:
169 raise Exception("Internal Error: horizontal_target_length={}"
170 " is not in range {} to {}"
171 .format(target_length, accum_uvlens[0],
172 accum_uvlens[-1]))
174 return target_uv
177 # --------------------- LOOP STRUCTURE ----------------------
179 # loops[hidx][vidx][pidx]
180 # hidx: horizontal index
181 # vidx: vertical index
182 # pidx: pair index
184 # <----- horizontal ----->
186 # (hidx, vidx, pidx) = (0, 3, 0)
187 # | (hidx, vidx, pidx) = (1, 3, 0)
188 # v v
189 # ^ o --- oo --- o
190 # | | || |
191 # vertical | o --- oo --- o <- (hidx, vidx, pidx)
192 # | o --- oo --- o = (1, 2, 1)
193 # | | || |
194 # v o --- oo --- o
195 # ^ ^
196 # | (hidx, vidx, pidx) = (1, 0, 1)
197 # (hidx, vidx, pidx) = (0, 0, 0)
199 # -----------------------------------------------------------
202 # get vertical differential of UV influenced by mesh vertex
203 def _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl):
204 common.debug_print(
205 "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx))
207 base_uv = loop_seqs[hidx][0][pidx][uv_layer].uv.copy()
209 # calculate original length
210 vloops = []
211 for s in loop_seqs[hidx]:
212 vloops.append(s[pidx])
213 total_vlen = _get_loop_vert_len(vloops)
214 accum_vlens = _get_loop_vert_accum_len(vloops)
215 total_uvlen = _get_loop_uv_len(vloops, uv_layer)
216 accum_uvlens = _get_loop_uv_accum_len(vloops, uv_layer)
217 orig_uvs = [l[uv_layer].uv.copy() for l in vloops]
219 # calculate target length
220 tgt_noinfl = total_uvlen * int((vidx + 1) / 2) * 2 / len(loop_seqs[hidx])
221 tgt_infl = total_uvlen * accum_vlens[vidx] / total_vlen
222 target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl
223 common.debug_print(target_length)
224 common.debug_print(accum_uvlens)
226 # calculate target UV
227 for i in range(len(accum_uvlens[:-1])):
228 # get line segment which UV will be placed
229 if accum_uvlens[i] <= target_length < accum_uvlens[i + 1]:
230 tgt_seg_len = target_length - accum_uvlens[i]
231 seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
232 uv1 = orig_uvs[i]
233 uv2 = orig_uvs[i + 1]
234 target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
235 break
236 elif i == (len(accum_uvlens[:-1]) - 1):
237 if abs(accum_uvlens[i + 1] - target_length) > 0.000001:
238 raise Exception("Internal Error: horizontal_target_length={}"
239 " is not equal to {}"
240 .format(target_length, accum_uvlens[-1]))
241 tgt_seg_len = target_length - accum_uvlens[i]
242 seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
243 uv1 = orig_uvs[i]
244 uv2 = orig_uvs[i + 1]
245 target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
246 break
247 else:
248 raise Exception("Internal Error: horizontal_target_length={}"
249 " is not in range {} to {}"
250 .format(target_length, accum_uvlens[0],
251 accum_uvlens[-1]))
253 return target_uv
256 # get horizontal differential of UV no influenced
257 def _get_hdiff_uv(uv_layer, loop_seqs, hidx):
258 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
259 h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv
261 return hidx * h_uv / len(loop_seqs)
264 # get vertical differential of UV no influenced
265 def _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx):
266 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
267 v_uv = loop_seqs[0][-1][0][uv_layer].uv.copy() - base_uv
269 hseq = loop_seqs[hidx]
270 return int((vidx + 1) / 2) * v_uv / (len(hseq) / 2)
273 @PropertyClassRegistry()
274 class _Properties:
275 idname = "align_uv"
277 @classmethod
278 def init_props(cls, scene):
279 scene.muv_align_uv_enabled = BoolProperty(
280 name="Align UV Enabled",
281 description="Align UV is enabled",
282 default=False
284 scene.muv_align_uv_transmission = BoolProperty(
285 name="Transmission",
286 description="Align linked UVs",
287 default=False
289 scene.muv_align_uv_select = BoolProperty(
290 name="Select",
291 description="Select UVs which are aligned",
292 default=False
294 scene.muv_align_uv_vertical = BoolProperty(
295 name="Vert-Infl (Vertical)",
296 description="Align vertical direction influenced "
297 "by mesh vertex proportion",
298 default=False
300 scene.muv_align_uv_horizontal = BoolProperty(
301 name="Vert-Infl (Horizontal)",
302 description="Align horizontal direction influenced "
303 "by mesh vertex proportion",
304 default=False
306 scene.muv_align_uv_mesh_infl = FloatProperty(
307 name="Mesh Influence",
308 description="Influence rate of mesh vertex",
309 min=0.0,
310 max=1.0,
311 default=0.0
313 scene.muv_align_uv_location = EnumProperty(
314 name="Location",
315 description="Align location",
316 items=[
317 ('LEFT_TOP', "Left/Top", "Align to Left or Top"),
318 ('MIDDLE', "Middle", "Align to middle"),
319 ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom")
321 default='MIDDLE'
324 scene.muv_align_uv_snap_method = EnumProperty(
325 name="Snap Method",
326 description="Snap method",
327 items=[
328 ('POINT', "Point", "Snap to point"),
329 ('EDGE', "Edge", "Snap to edge"),
331 default='POINT'
333 scene.muv_align_uv_snap_point_group = EnumProperty(
334 name="Snap Group (Point)",
335 description="Group that snap (point) operation applies for",
336 items=[
337 ('VERT', "Vertex", "Vertex"),
338 ('FACE', "Face", "Face"),
339 ('UV_ISLAND', "UV Island", "UV Island"),
341 default='FACE'
343 scene.muv_align_uv_snap_point_target = FloatVectorProperty(
344 name="Snap Target (Point)",
345 description="Target point where UV vertices snap to",
346 size=2,
347 precision=4,
348 soft_min=-1.0,
349 soft_max=1.0,
350 step=1,
351 default=(0.000, 0.000),
353 scene.muv_align_uv_snap_edge_group = EnumProperty(
354 name="Snap Group (Edge)",
355 description="Group that snap (edge) operation applies for",
356 items=[
357 ('EDGE', "Edge", "Edge"),
358 ('FACE', "Face", "Face"),
359 ('UV_ISLAND', "UV Island", "UV Island"),
361 default='FACE'
363 scene.muv_align_uv_snap_edge_target_1 = FloatVectorProperty(
364 name="Snap Target (Edge)",
365 description="Target edge where UV vertices snap to",
366 size=2,
367 precision=4,
368 soft_min=-1.0,
369 soft_max=1.0,
370 step=1,
371 default=(0.000, 0.000),
373 scene.muv_align_uv_snap_edge_target_2 = FloatVectorProperty(
374 name="Snap Target (Edge)",
375 description="Target edge where UV vertices snap to",
376 size=2,
377 precision=4,
378 soft_min=-1.0,
379 soft_max=1.0,
380 step=1,
381 default=(0.000, 0.000),
384 @classmethod
385 def del_props(cls, scene):
386 del scene.muv_align_uv_enabled
387 del scene.muv_align_uv_transmission
388 del scene.muv_align_uv_select
389 del scene.muv_align_uv_vertical
390 del scene.muv_align_uv_horizontal
391 del scene.muv_align_uv_mesh_infl
392 del scene.muv_align_uv_location
393 del scene.muv_align_uv_snap_method
394 del scene.muv_align_uv_snap_point_group
395 del scene.muv_align_uv_snap_point_target
396 del scene.muv_align_uv_snap_edge_group
397 del scene.muv_align_uv_snap_edge_target_1
398 del scene.muv_align_uv_snap_edge_target_2
401 @BlClassRegistry()
402 @compat.make_annotations
403 class MUV_OT_AlignUV_Circle(bpy.types.Operator):
405 bl_idname = "uv.muv_align_uv_circle"
406 bl_label = "Align UV (Circle)"
407 bl_description = "Align UV coordinates to Circle"
408 bl_options = {'REGISTER', 'UNDO'}
410 transmission = BoolProperty(
411 name="Transmission",
412 description="Align linked UVs",
413 default=False
415 select = BoolProperty(
416 name="Select",
417 description="Select UVs which are aligned",
418 default=False
421 @classmethod
422 def poll(cls, context):
423 # we can not get area/space/region from console
424 if common.is_console_mode():
425 return True
426 return _is_valid_context(context)
428 def execute(self, context):
429 objs = common.get_uv_editable_objects(context)
431 for obj in objs:
432 bm = bmesh.from_edit_mesh(obj.data)
433 if common.check_version(2, 73, 0) >= 0:
434 bm.faces.ensure_lookup_table()
435 uv_layer = bm.loops.layers.uv.verify()
437 # loop_seqs[horizontal][vertical][loop]
438 loop_seqs, error = common.get_loop_sequences(bm, uv_layer, True)
439 if not loop_seqs:
440 self.report({'WARNING'},
441 "Object {}: {}".format(obj.name, error))
442 return {'CANCELLED'}
444 # get circle and new UVs
445 uvs = [hseq[0][0][uv_layer].uv.copy() for hseq in loop_seqs]
446 c, r = _get_circle(uvs[0:3])
447 new_uvs = _calc_v_on_circle(uvs, c, r)
449 # check if center is identical
450 center_is_identical = False
451 center = loop_seqs[0][-1][0].vert
452 if (len(loop_seqs[0][-1]) == 1) and \
453 loop_seqs[0][-1][0].vert == center:
454 center_is_identical = True
456 # check if topology is correct
457 if center_is_identical:
458 for hseq in loop_seqs[1:]:
459 if len(hseq[-1]) != 1:
460 self.report({'WARNING'},
461 "Object {}: Last face must be triangle"
462 .format(obj.name))
463 return {'CANCELLED'}
464 if hseq[-1][0].vert != center:
465 self.report({'WARNING'},
466 "Object {}: Center must be identical"
467 .format(obj.name))
468 return {'CANCELLED'}
469 else:
470 for hseq in loop_seqs[1:]:
471 if len(hseq[-1]) == 1:
472 self.report({'WARNING'},
473 "Object {}: Last face must not be triangle"
474 .format(obj.name))
475 return {'CANCELLED'}
476 if hseq[-1][0].vert == center:
477 self.report({'WARNING'},
478 "Object {}: Center must not be identical"
479 .format(obj.name))
480 return {'CANCELLED'}
482 # align to circle
483 if self.transmission:
484 for hidx, hseq in enumerate(loop_seqs):
485 for vidx, pair in enumerate(hseq):
486 all_ = int((len(hseq) + 1) / 2)
487 if center_is_identical:
488 r = (all_ - int((vidx + 1) / 2)) / all_
489 else:
490 r = (1 + all_ - int((vidx + 1) / 2)) / all_
491 pair[0][uv_layer].uv = c + (new_uvs[hidx] - c) * r
492 if self.select:
493 pair[0][uv_layer].select = True
495 if len(pair) < 2:
496 continue
497 # for quad polygon
498 next_hidx = (hidx + 1) % len(loop_seqs)
499 pair[1][uv_layer].uv = \
500 c + ((new_uvs[next_hidx]) - c) * r
501 if self.select:
502 pair[1][uv_layer].select = True
503 else:
504 for hidx, hseq in enumerate(loop_seqs):
505 pair = hseq[0]
506 pair[0][uv_layer].uv = new_uvs[hidx]
507 pair[1][uv_layer].uv = new_uvs[(hidx + 1) % len(loop_seqs)]
508 if self.select:
509 pair[0][uv_layer].select = True
510 pair[1][uv_layer].select = True
512 bmesh.update_edit_mesh(obj.data)
514 return {'FINISHED'}
517 @BlClassRegistry()
518 @compat.make_annotations
519 class MUV_OT_AlignUV_Straighten(bpy.types.Operator):
521 bl_idname = "uv.muv_align_uv_straighten"
522 bl_label = "Align UV (Straighten)"
523 bl_description = "Straighten UV coordinates"
524 bl_options = {'REGISTER', 'UNDO'}
526 transmission = BoolProperty(
527 name="Transmission",
528 description="Align linked UVs",
529 default=False
531 select = BoolProperty(
532 name="Select",
533 description="Select UVs which are aligned",
534 default=False
536 vertical = BoolProperty(
537 name="Vert-Infl (Vertical)",
538 description="Align vertical direction influenced "
539 "by mesh vertex proportion",
540 default=False
542 horizontal = BoolProperty(
543 name="Vert-Infl (Horizontal)",
544 description="Align horizontal direction influenced "
545 "by mesh vertex proportion",
546 default=False
548 mesh_infl = FloatProperty(
549 name="Mesh Influence",
550 description="Influence rate of mesh vertex",
551 min=0.0,
552 max=1.0,
553 default=0.0
556 @classmethod
557 def poll(cls, context):
558 # we can not get area/space/region from console
559 if common.is_console_mode():
560 return True
561 return _is_valid_context(context)
563 # selected and paralleled UV loop sequence will be aligned
564 def __align_w_transmission(self, loop_seqs, uv_layer):
565 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
567 # calculate diff UVs
568 diff_uvs = []
569 # hseq[vertical][loop]
570 for hidx, hseq in enumerate(loop_seqs):
571 # pair[loop]
572 diffs = []
573 for vidx in range(0, len(hseq), 2):
574 if self.horizontal:
575 hdiff_uvs = [
576 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
577 self.mesh_infl),
578 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
579 self.mesh_infl),
580 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
581 hidx, 0, self.mesh_infl),
582 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
583 hidx, 1, self.mesh_infl),
585 else:
586 hdiff_uvs = [
587 _get_hdiff_uv(uv_layer, loop_seqs, hidx),
588 _get_hdiff_uv(uv_layer, loop_seqs, hidx + 1),
589 _get_hdiff_uv(uv_layer, loop_seqs, hidx),
590 _get_hdiff_uv(uv_layer, loop_seqs, hidx + 1)
592 if self.vertical:
593 vdiff_uvs = [
594 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
595 self.mesh_infl),
596 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
597 self.mesh_infl),
598 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
599 hidx, 0, self.mesh_infl),
600 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
601 hidx, 1, self.mesh_infl),
603 else:
604 vdiff_uvs = [
605 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
606 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
607 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
608 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
610 diffs.append([hdiff_uvs, vdiff_uvs])
611 diff_uvs.append(diffs)
613 # update UV
614 for hseq, diffs in zip(loop_seqs, diff_uvs):
615 for vidx in range(0, len(hseq), 2):
616 loops = [
617 hseq[vidx][0], hseq[vidx][1],
618 hseq[vidx + 1][0], hseq[vidx + 1][1]
620 for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
621 diffs[int(vidx / 2)][1]):
622 l[uv_layer].uv = base_uv + hdiff + vdiff
623 if self.select:
624 l[uv_layer].select = True
626 # only selected UV loop sequence will be aligned
627 def __align_wo_transmission(self, loop_seqs, uv_layer):
628 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
630 h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv
631 for hidx, hseq in enumerate(loop_seqs):
632 # only selected loop pair is targeted
633 pair = hseq[0]
634 hdiff_uv_0 = hidx * h_uv / len(loop_seqs)
635 hdiff_uv_1 = (hidx + 1) * h_uv / len(loop_seqs)
636 pair[0][uv_layer].uv = base_uv + hdiff_uv_0
637 pair[1][uv_layer].uv = base_uv + hdiff_uv_1
638 if self.select:
639 pair[0][uv_layer].select = True
640 pair[1][uv_layer].select = True
642 def __align(self, loop_seqs, uv_layer):
643 if self.transmission:
644 self.__align_w_transmission(loop_seqs, uv_layer)
645 else:
646 self.__align_wo_transmission(loop_seqs, uv_layer)
648 def execute(self, context):
649 objs = common.get_uv_editable_objects(context)
651 for obj in objs:
652 bm = bmesh.from_edit_mesh(obj.data)
653 if common.check_version(2, 73, 0) >= 0:
654 bm.faces.ensure_lookup_table()
655 uv_layer = bm.loops.layers.uv.verify()
657 # loop_seqs[horizontal][vertical][loop]
658 loop_seqs, error = common.get_loop_sequences(bm, uv_layer)
659 if not loop_seqs:
660 self.report({'WARNING'},
661 "Object {}: {}".format(obj.name, error))
662 return {'CANCELLED'}
664 # align
665 self.__align(loop_seqs, uv_layer)
667 bmesh.update_edit_mesh(obj.data)
669 return {'FINISHED'}
672 @BlClassRegistry()
673 @compat.make_annotations
674 class MUV_OT_AlignUV_Axis(bpy.types.Operator):
676 bl_idname = "uv.muv_align_uv_axis"
677 bl_label = "Align UV (XY-Axis)"
678 bl_description = "Align UV to XY-axis"
679 bl_options = {'REGISTER', 'UNDO'}
681 transmission = BoolProperty(
682 name="Transmission",
683 description="Align linked UVs",
684 default=False
686 select = BoolProperty(
687 name="Select",
688 description="Select UVs which are aligned",
689 default=False
691 vertical = BoolProperty(
692 name="Vert-Infl (Vertical)",
693 description="Align vertical direction influenced "
694 "by mesh vertex proportion",
695 default=False
697 horizontal = BoolProperty(
698 name="Vert-Infl (Horizontal)",
699 description="Align horizontal direction influenced "
700 "by mesh vertex proportion",
701 default=False
703 location = EnumProperty(
704 name="Location",
705 description="Align location",
706 items=[
707 ('LEFT_TOP', "Left/Top", "Align to Left or Top"),
708 ('MIDDLE', "Middle", "Align to middle"),
709 ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom")
711 default='MIDDLE'
713 mesh_infl = FloatProperty(
714 name="Mesh Influence",
715 description="Influence rate of mesh vertex",
716 min=0.0,
717 max=1.0,
718 default=0.0
721 @classmethod
722 def poll(cls, context):
723 # we can not get area/space/region from console
724 if common.is_console_mode():
725 return True
726 return _is_valid_context(context)
728 # get min/max of UV
729 def __get_uv_max_min(self, loop_seqs, uv_layer):
730 uv_max = Vector((-1000000.0, -1000000.0))
731 uv_min = Vector((1000000.0, 1000000.0))
732 for hseq in loop_seqs:
733 for l in hseq[0]:
734 uv = l[uv_layer].uv
735 uv_max.x = max(uv.x, uv_max.x)
736 uv_max.y = max(uv.y, uv_max.y)
737 uv_min.x = min(uv.x, uv_min.x)
738 uv_min.y = min(uv.y, uv_min.y)
740 return uv_max, uv_min
742 # get UV differentiation when UVs are aligned to X-axis
743 def __get_x_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min,
744 width, height):
745 diff_uvs = []
746 for hidx, hseq in enumerate(loop_seqs):
747 pair = hseq[0]
748 luv0 = pair[0][uv_layer]
749 luv1 = pair[1][uv_layer]
750 target_uv0 = Vector((0.0, 0.0))
751 target_uv1 = Vector((0.0, 0.0))
752 if self.location == 'RIGHT_BOTTOM':
753 target_uv0.y = target_uv1.y = uv_min.y
754 elif self.location == 'MIDDLE':
755 target_uv0.y = target_uv1.y = uv_min.y + height * 0.5
756 elif self.location == 'LEFT_TOP':
757 target_uv0.y = target_uv1.y = uv_min.y + height
758 if luv0.uv.x < luv1.uv.x:
759 target_uv0.x = uv_min.x + hidx * width / len(loop_seqs)
760 target_uv1.x = uv_min.x + (hidx + 1) * width / len(loop_seqs)
761 else:
762 target_uv0.x = uv_min.x + (hidx + 1) * width / len(loop_seqs)
763 target_uv1.x = uv_min.x + hidx * width / len(loop_seqs)
764 diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv])
766 return diff_uvs
768 # get UV differentiation when UVs are aligned to Y-axis
769 def __get_y_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min,
770 width, height):
771 diff_uvs = []
772 for hidx, hseq in enumerate(loop_seqs):
773 pair = hseq[0]
774 luv0 = pair[0][uv_layer]
775 luv1 = pair[1][uv_layer]
776 target_uv0 = Vector((0.0, 0.0))
777 target_uv1 = Vector((0.0, 0.0))
778 if self.location == 'RIGHT_BOTTOM':
779 target_uv0.x = target_uv1.x = uv_min.x + width
780 elif self.location == 'MIDDLE':
781 target_uv0.x = target_uv1.x = uv_min.x + width * 0.5
782 elif self.location == 'LEFT_TOP':
783 target_uv0.x = target_uv1.x = uv_min.x
784 if luv0.uv.y < luv1.uv.y:
785 target_uv0.y = uv_min.y + hidx * height / len(loop_seqs)
786 target_uv1.y = uv_min.y + (hidx + 1) * height / len(loop_seqs)
787 else:
788 target_uv0.y = uv_min.y + (hidx + 1) * height / len(loop_seqs)
789 target_uv1.y = uv_min.y + hidx * height / len(loop_seqs)
790 diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv])
792 return diff_uvs
794 # only selected UV loop sequence will be aligned along to X-axis
795 def __align_to_x_axis_wo_transmission(self, loop_seqs, uv_layer,
796 uv_min, width, height):
797 # reverse if the UV coordinate is not sorted by position
798 need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \
799 loop_seqs[-1][0][0][uv_layer].uv.x
800 if need_revese:
801 loop_seqs.reverse()
802 for hidx, hseq in enumerate(loop_seqs):
803 for vidx, pair in enumerate(hseq):
804 tmp = loop_seqs[hidx][vidx][0]
805 loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
806 loop_seqs[hidx][vidx][1] = tmp
808 # get UV differential
809 diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs,
810 uv_layer, uv_min,
811 width, height)
813 # update UV
814 for hseq, duv in zip(loop_seqs, diff_uvs):
815 pair = hseq[0]
816 luv0 = pair[0][uv_layer]
817 luv1 = pair[1][uv_layer]
818 luv0.uv = luv0.uv + duv[0]
819 luv1.uv = luv1.uv + duv[1]
821 # only selected UV loop sequence will be aligned along to Y-axis
822 def __align_to_y_axis_wo_transmission(self, loop_seqs, uv_layer,
823 uv_min, width, height):
824 # reverse if the UV coordinate is not sorted by position
825 need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \
826 loop_seqs[-1][0][0][uv_layer].uv.y
827 if need_revese:
828 loop_seqs.reverse()
829 for hidx, hseq in enumerate(loop_seqs):
830 for vidx, pair in enumerate(hseq):
831 tmp = loop_seqs[hidx][vidx][0]
832 loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
833 loop_seqs[hidx][vidx][1] = tmp
835 # get UV differential
836 diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs,
837 uv_layer, uv_min,
838 width, height)
840 # update UV
841 for hseq, duv in zip(loop_seqs, diff_uvs):
842 pair = hseq[0]
843 luv0 = pair[0][uv_layer]
844 luv1 = pair[1][uv_layer]
845 luv0.uv = luv0.uv + duv[0]
846 luv1.uv = luv1.uv + duv[1]
848 # selected and paralleled UV loop sequence will be aligned along to X-axis
849 def __align_to_x_axis_w_transmission(self, loop_seqs, uv_layer,
850 uv_min, width, height):
851 # reverse if the UV coordinate is not sorted by position
852 need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \
853 loop_seqs[-1][0][0][uv_layer].uv.x
854 if need_revese:
855 loop_seqs.reverse()
856 for hidx, hseq in enumerate(loop_seqs):
857 for vidx in range(len(hseq)):
858 tmp = loop_seqs[hidx][vidx][0]
859 loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
860 loop_seqs[hidx][vidx][1] = tmp
862 # get offset UVs when the UVs are aligned to X-axis
863 align_diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs,
864 uv_layer, uv_min,
865 width, height)
866 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
867 offset_uvs = []
868 for hseq, aduv in zip(loop_seqs, align_diff_uvs):
869 luv0 = hseq[0][0][uv_layer]
870 luv1 = hseq[0][1][uv_layer]
871 offset_uvs.append([luv0.uv + aduv[0] - base_uv,
872 luv1.uv + aduv[1] - base_uv])
874 # get UV differential
875 diff_uvs = []
876 # hseq[vertical][loop]
877 for hidx, hseq in enumerate(loop_seqs):
878 # pair[loop]
879 diffs = []
880 for vidx in range(0, len(hseq), 2):
881 if self.horizontal:
882 hdiff_uvs = [
883 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
884 self.mesh_infl),
885 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
886 self.mesh_infl),
887 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
888 hidx, 0, self.mesh_infl),
889 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
890 hidx, 1, self.mesh_infl),
892 hdiff_uvs[0].y = hdiff_uvs[0].y + offset_uvs[hidx][0].y
893 hdiff_uvs[1].y = hdiff_uvs[1].y + offset_uvs[hidx][1].y
894 hdiff_uvs[2].y = hdiff_uvs[2].y + offset_uvs[hidx][0].y
895 hdiff_uvs[3].y = hdiff_uvs[3].y + offset_uvs[hidx][1].y
896 else:
897 hdiff_uvs = [
898 offset_uvs[hidx][0],
899 offset_uvs[hidx][1],
900 offset_uvs[hidx][0],
901 offset_uvs[hidx][1],
903 if self.vertical:
904 vdiff_uvs = [
905 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
906 self.mesh_infl),
907 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
908 self.mesh_infl),
909 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
910 hidx, 0, self.mesh_infl),
911 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
912 hidx, 1, self.mesh_infl),
914 else:
915 vdiff_uvs = [
916 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
917 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
918 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
919 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
921 diffs.append([hdiff_uvs, vdiff_uvs])
922 diff_uvs.append(diffs)
924 # update UV
925 for hseq, diffs in zip(loop_seqs, diff_uvs):
926 for vidx in range(0, len(hseq), 2):
927 loops = [
928 hseq[vidx][0], hseq[vidx][1],
929 hseq[vidx + 1][0], hseq[vidx + 1][1]
931 for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
932 diffs[int(vidx / 2)][1]):
933 l[uv_layer].uv = base_uv + hdiff + vdiff
934 if self.select:
935 l[uv_layer].select = True
937 # selected and paralleled UV loop sequence will be aligned along to Y-axis
938 def __align_to_y_axis_w_transmission(self, loop_seqs, uv_layer,
939 uv_min, width, height):
940 # reverse if the UV coordinate is not sorted by position
941 need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \
942 loop_seqs[-1][0][-1][uv_layer].uv.y
943 if need_revese:
944 loop_seqs.reverse()
945 for hidx, hseq in enumerate(loop_seqs):
946 for vidx in range(len(hseq)):
947 tmp = loop_seqs[hidx][vidx][0]
948 loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
949 loop_seqs[hidx][vidx][1] = tmp
951 # get offset UVs when the UVs are aligned to Y-axis
952 align_diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs,
953 uv_layer, uv_min,
954 width, height)
955 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
956 offset_uvs = []
957 for hseq, aduv in zip(loop_seqs, align_diff_uvs):
958 luv0 = hseq[0][0][uv_layer]
959 luv1 = hseq[0][1][uv_layer]
960 offset_uvs.append([luv0.uv + aduv[0] - base_uv,
961 luv1.uv + aduv[1] - base_uv])
963 # get UV differential
964 diff_uvs = []
965 # hseq[vertical][loop]
966 for hidx, hseq in enumerate(loop_seqs):
967 # pair[loop]
968 diffs = []
969 for vidx in range(0, len(hseq), 2):
970 if self.horizontal:
971 hdiff_uvs = [
972 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
973 self.mesh_infl),
974 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
975 self.mesh_infl),
976 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
977 hidx, 0, self.mesh_infl),
978 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
979 hidx, 1, self.mesh_infl),
981 hdiff_uvs[0].x = hdiff_uvs[0].x + offset_uvs[hidx][0].x
982 hdiff_uvs[1].x = hdiff_uvs[1].x + offset_uvs[hidx][1].x
983 hdiff_uvs[2].x = hdiff_uvs[2].x + offset_uvs[hidx][0].x
984 hdiff_uvs[3].x = hdiff_uvs[3].x + offset_uvs[hidx][1].x
985 else:
986 hdiff_uvs = [
987 offset_uvs[hidx][0],
988 offset_uvs[hidx][1],
989 offset_uvs[hidx][0],
990 offset_uvs[hidx][1],
992 if self.vertical:
993 vdiff_uvs = [
994 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
995 self.mesh_infl),
996 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
997 self.mesh_infl),
998 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
999 hidx, 0, self.mesh_infl),
1000 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
1001 hidx, 1, self.mesh_infl),
1003 else:
1004 vdiff_uvs = [
1005 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
1006 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
1007 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
1008 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
1010 diffs.append([hdiff_uvs, vdiff_uvs])
1011 diff_uvs.append(diffs)
1013 # update UV
1014 for hseq, diffs in zip(loop_seqs, diff_uvs):
1015 for vidx in range(0, len(hseq), 2):
1016 loops = [
1017 hseq[vidx][0], hseq[vidx][1],
1018 hseq[vidx + 1][0], hseq[vidx + 1][1]
1020 for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
1021 diffs[int(vidx / 2)][1]):
1022 l[uv_layer].uv = base_uv + hdiff + vdiff
1023 if self.select:
1024 l[uv_layer].select = True
1026 def __align(self, loop_seqs, uv_layer, uv_min, width, height):
1027 # align along to x-axis
1028 if width > height:
1029 if self.transmission:
1030 self.__align_to_x_axis_w_transmission(loop_seqs,
1031 uv_layer, uv_min,
1032 width, height)
1033 else:
1034 self.__align_to_x_axis_wo_transmission(loop_seqs,
1035 uv_layer, uv_min,
1036 width, height)
1037 # align along to y-axis
1038 else:
1039 if self.transmission:
1040 self.__align_to_y_axis_w_transmission(loop_seqs,
1041 uv_layer, uv_min,
1042 width, height)
1043 else:
1044 self.__align_to_y_axis_wo_transmission(loop_seqs,
1045 uv_layer, uv_min,
1046 width, height)
1048 def execute(self, context):
1049 objs = common.get_uv_editable_objects(context)
1051 for obj in objs:
1052 bm = bmesh.from_edit_mesh(obj.data)
1053 if common.check_version(2, 73, 0) >= 0:
1054 bm.faces.ensure_lookup_table()
1055 uv_layer = bm.loops.layers.uv.verify()
1057 # loop_seqs[horizontal][vertical][loop]
1058 loop_seqs, error = common.get_loop_sequences(bm, uv_layer)
1059 if not loop_seqs:
1060 self.report({'WARNING'},
1061 "Object {}: {}".format(obj.name, error))
1062 return {'CANCELLED'}
1064 # get height and width
1065 uv_max, uv_min = self.__get_uv_max_min(loop_seqs, uv_layer)
1066 width = uv_max.x - uv_min.x
1067 height = uv_max.y - uv_min.y
1069 self.__align(loop_seqs, uv_layer, uv_min, width, height)
1071 bmesh.update_edit_mesh(obj.data)
1073 return {'FINISHED'}
1076 @BlClassRegistry()
1077 @compat.make_annotations
1078 class MUV_OT_AlignUV_SnapToPoint(bpy.types.Operator):
1080 bl_idname = "uv.muv_align_uv_snap_to_point"
1081 bl_label = "Align UV (Snap to Point)"
1082 bl_description = "Align UV to the target point"
1083 bl_options = {'REGISTER', 'UNDO'}
1085 group = EnumProperty(
1086 name="Snap Group",
1087 description="Group that snap operation applies for",
1088 items=[
1089 ('VERT', "Vertex", "Vertex"),
1090 ('FACE', "Face", "Face"),
1091 ('UV_ISLAND', "UV Island", "UV Island"),
1093 default='FACE'
1095 target = FloatVectorProperty(
1096 name="Snap Target",
1097 description="Target where UV vertices snap to",
1098 size=2,
1099 precision=4,
1100 soft_min=-1.0,
1101 soft_max=1.0,
1102 step=1,
1103 default=(0.000, 0.000),
1106 def _get_snap_target_loops(self, context, bm, uv_layer):
1107 target_loops = []
1109 selected_faces = [f for f in bm.faces if f.select]
1111 # Process snap operation.
1112 for face in selected_faces:
1113 for l in face.loops:
1114 if context.tool_settings.use_uv_select_sync or \
1115 l[uv_layer].select:
1116 target_loops.append(l)
1118 return target_loops
1120 def _get_snap_target_faces(self, context, bm, uv_layer):
1121 target_faces = []
1123 selected_faces = [f for f in bm.faces if f.select]
1125 for face in selected_faces:
1126 for l in face.loops:
1127 if not context.tool_settings.use_uv_select_sync and \
1128 not l[uv_layer].select:
1129 break
1130 else:
1131 target_faces.append(face)
1133 return target_faces
1135 def _get_snap_target_islands(self, context, bm, uv_layer):
1136 target_islands = []
1138 islands = common.get_island_info_from_bmesh(bm, only_selected=True)
1140 for isl in islands:
1141 some_verts_not_selected = False
1142 for face in isl["faces"]:
1143 for l in face["face"].loops:
1144 if not context.tool_settings.use_uv_select_sync and \
1145 not l[uv_layer].select:
1146 some_verts_not_selected = True
1147 break
1148 if not some_verts_not_selected:
1149 target_islands.append(isl)
1151 return target_islands
1153 def execute(self, context):
1154 objs = common.get_uv_editable_objects(context)
1156 group_to_reason = {
1157 'VERT': "Vertex",
1158 'FACE': "Face",
1159 'UV_ISLAND': "UV Island",
1161 no_selection_reason = group_to_reason[self.group]
1163 for obj in objs:
1164 bm = bmesh.from_edit_mesh(obj.data)
1165 if common.check_version(2, 73, 0) >= 0:
1166 bm.faces.ensure_lookup_table()
1167 uv_layer = bm.loops.layers.uv.verify()
1169 if self.group == 'VERT':
1170 target_loops = \
1171 self._get_snap_target_loops(context, bm, uv_layer)
1173 # Process snap operation.
1174 for l in target_loops:
1175 l[uv_layer].uv = self.target
1176 no_selection_reason = None
1178 elif self.group == 'FACE':
1179 target_faces = \
1180 self._get_snap_target_faces(context, bm, uv_layer)
1182 for face in target_faces:
1183 ave_uv = Vector((0.0, 0.0))
1184 for l in face.loops:
1185 ave_uv += l[uv_layer].uv
1186 ave_uv /= len(face.loops)
1187 diff = Vector(self.target) - ave_uv
1189 # Process snap operation.
1190 for l in face.loops:
1191 l[uv_layer].uv += diff
1192 no_selection_reason = None
1194 elif self.group == 'UV_ISLAND':
1195 target_islands = \
1196 self._get_snap_target_islands(context, bm, uv_layer)
1198 for isl in target_islands:
1199 ave_uv = Vector((0.0, 0.0))
1200 count = 0
1201 for face in isl["faces"]:
1202 for l in face["face"].loops:
1203 ave_uv += l[uv_layer].uv
1204 count += 1
1205 if count != 0:
1206 ave_uv /= count
1207 diff = Vector(self.target) - ave_uv
1209 # Process snap operation.
1210 for face in isl["faces"]:
1211 for l in face["face"].loops:
1212 l[uv_layer].uv += diff
1213 no_selection_reason = None
1215 bmesh.update_edit_mesh(obj.data)
1217 if no_selection_reason:
1218 self.report(
1219 {'WARNING'},
1220 "Must select more than 1 {}.".format(no_selection_reason)
1222 return {'CANCELLED'}
1224 return {'FINISHED'}
1227 @BlClassRegistry()
1228 @compat.make_annotations
1229 class MUV_OT_AlignUV_Snap_SetPointTargetToCursor(bpy.types.Operator):
1231 bl_idname = "uv.muv_align_uv_snap_set_point_target_to_cursor"
1232 bl_label = "Set Point Target to Cursor"
1233 bl_description = """Set point target to the cursor for
1234 'Align UV (Snap to Point)'"""
1235 bl_options = {'REGISTER', 'UNDO'}
1237 def execute(self, context):
1238 sc = context.scene
1240 _, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
1241 'IMAGE_EDITOR')
1242 cursor_loc = space.cursor_location
1244 sc.muv_align_uv_snap_point_target = cursor_loc
1246 return {'FINISHED'}
1249 @BlClassRegistry()
1250 @compat.make_annotations
1251 class MUV_OT_AlignUV_Snap_SetPointTargetToVertexGroup(bpy.types.Operator):
1253 bl_idname = "uv.muv_align_uv_snap_set_point_target_to_vertex_group"
1254 bl_label = "Set Point Target to Vertex Group"
1255 bl_description = """Set point target to the average of vertices for
1256 'Align UV (Snap to Point)'"""
1257 bl_options = {'REGISTER', 'UNDO'}
1259 def execute(self, context):
1260 sc = context.scene
1261 objs = common.get_uv_editable_objects(context)
1263 ave_uv = Vector((0.0, 0.0))
1264 count = 0
1265 for obj in objs:
1266 bm = bmesh.from_edit_mesh(obj.data)
1267 if common.check_version(2, 73, 0) >= 0:
1268 bm.faces.ensure_lookup_table()
1269 uv_layer = bm.loops.layers.uv.verify()
1271 selected_faces = [f for f in bm.faces if f.select]
1272 for face in selected_faces:
1273 for l in face.loops:
1274 if context.tool_settings.use_uv_select_sync or \
1275 l[uv_layer].select:
1276 ave_uv += l[uv_layer].uv
1277 count += 1
1278 if count != 0:
1279 ave_uv /= count
1281 sc.muv_align_uv_snap_point_target = ave_uv
1283 return {'FINISHED'}
1286 @BlClassRegistry()
1287 @compat.make_annotations
1288 class MUV_OT_AlignUV_SnapToEdge(bpy.types.Operator):
1290 bl_idname = "uv.muv_align_uv_snap_to_edge"
1291 bl_label = "Align UV (Snap to Edge)"
1292 bl_description = "Align UV to the target edge"
1293 bl_options = {'REGISTER', 'UNDO'}
1295 group = EnumProperty(
1296 name="Snap Group",
1297 description="Group that snap operation applies for",
1298 items=[
1299 ('EDGE', "Edge", "Edge"),
1300 ('FACE', "Face", "Face"),
1301 ('UV_ISLAND', "UV Island", "UV Island"),
1303 default='FACE'
1305 target_1 = FloatVectorProperty(
1306 name="Snap Target 1",
1307 description="Vertex 1 of the target edge",
1308 size=2,
1309 precision=4,
1310 soft_min=-1.0,
1311 soft_max=1.0,
1312 step=1,
1313 default=(0.000, 0.000),
1315 target_2 = FloatVectorProperty(
1316 name="Snap Target 2",
1317 description="Vertex 2 of the target edge",
1318 size=2,
1319 precision=4,
1320 soft_min=-1.0,
1321 soft_max=1.0,
1322 step=1,
1323 default=(0.000, 0.000),
1326 def _calc_snap_move_amount(self, loops, uv_layer):
1327 ave = (loops[0][uv_layer].uv + loops[1][uv_layer].uv) / 2
1328 target = (Vector(self.target_1) + Vector(self.target_2)) / 2
1330 return target - ave
1332 def _get_snap_target_loop_pairs(self, bm, uv_layer):
1333 target_loop_pairs = []
1335 selected_edges = [e for e in bm.edges if e.select]
1337 cand_loops = []
1338 for edge in selected_edges:
1339 for l in edge.link_loops:
1340 if l[uv_layer].select:
1341 cand_loops.append(l)
1343 for l in cand_loops:
1344 if l[uv_layer].select and l.link_loop_next[uv_layer].select:
1345 d = {l, l.link_loop_next}
1346 if d not in target_loop_pairs:
1347 assert l.face == l.link_loop_next.face
1348 target_loop_pairs.append(d)
1350 return target_loop_pairs
1352 def _find_target_island_from_face(self, islands, face):
1353 for isl in islands:
1354 for f in isl["faces"]:
1355 if f["face"] == face:
1356 return isl
1358 return None
1360 def execute(self, context):
1361 objs = common.get_uv_editable_objects(context)
1363 no_selection = True
1364 for obj in objs:
1365 bm = bmesh.from_edit_mesh(obj.data)
1366 if common.check_version(2, 73, 0) >= 0:
1367 bm.faces.ensure_lookup_table()
1368 uv_layer = bm.loops.layers.uv.verify()
1370 if self.group == 'EDGE':
1371 target_loop_pairs = \
1372 self._get_snap_target_loop_pairs(bm, uv_layer)
1374 for pair in target_loop_pairs:
1375 p = list(pair)
1376 diff = self._calc_snap_move_amount(p, uv_layer)
1378 # Process snap operation.
1379 p[0][uv_layer].uv += diff
1380 p[1][uv_layer].uv += diff
1382 no_selection = False
1384 elif self.group == 'FACE':
1385 target_loop_pairs = \
1386 self._get_snap_target_loop_pairs(bm, uv_layer)
1388 face_processed = []
1389 for pair in target_loop_pairs:
1390 p = list(pair)
1391 diff = self._calc_snap_move_amount(p, uv_layer)
1393 # Process snap operation.
1394 face = p[0].face
1395 if face in face_processed:
1396 self.report(
1397 {'WARNING'},
1398 "Must select only one edge per face. (Object: {})"
1399 .format(obj.name)
1401 return {'CANCELLED'}
1402 face_processed.append(face)
1403 for l in face.loops:
1404 l[uv_layer].uv += diff
1405 no_selection = False
1407 elif self.group == 'UV_ISLAND':
1408 target_loop_pairs = \
1409 self._get_snap_target_loop_pairs(bm, uv_layer)
1411 islands = common.get_island_info_from_bmesh(
1412 bm, only_selected=False)
1414 isl_processed = []
1415 for pair in target_loop_pairs:
1416 p = list(pair)
1417 diff = self._calc_snap_move_amount(p, uv_layer)
1419 # Find island to process.
1420 face = p[0].face
1421 target_isl = \
1422 self._find_target_island_from_face(islands, face)
1423 if target_isl is None:
1424 self.report(
1425 {'WARNING'},
1426 "Failed to find island. (Object: {})"
1427 .format(obj.name)
1429 return {'CANCELLED'}
1430 if target_isl in isl_processed:
1431 self.report(
1432 {'WARNING'},
1433 """Must select only one edge per island.
1434 (Object: {})"""
1435 .format(obj.name)
1437 return {'CANCELLED'}
1438 isl_processed.append(target_isl)
1440 # Process snap operation.
1441 for f in target_isl["faces"]:
1442 for l in f["face"].loops:
1443 l[uv_layer].uv += diff
1444 no_selection = False
1446 bmesh.update_edit_mesh(obj.data)
1448 if no_selection:
1449 self.report({'WARNING'}, "Must select more than 1 Edge.")
1450 return {'CANCELLED'}
1452 return {'FINISHED'}
1455 @BlClassRegistry()
1456 @compat.make_annotations
1457 class MUV_OT_AlignUV_Snap_SetEdgeTargetToEdgeCenter(bpy.types.Operator):
1459 bl_idname = "uv.muv_align_uv_snap_set_edge_target_to_edge_center"
1460 bl_label = "Set Edge Target to Edge Center"
1461 bl_description = """Set edge target to the center of edge for
1462 'Align UV (Snap to Edge)'"""
1463 bl_options = {'REGISTER', 'UNDO'}
1465 def _get_target_loop_pairs(self, bm, uv_layer):
1466 target_loop_pairs = []
1468 selected_edges = [e for e in bm.edges if e.select]
1470 cand_loops = []
1471 for edge in selected_edges:
1472 for l in edge.link_loops:
1473 if l[uv_layer].select:
1474 cand_loops.append(l)
1476 for l in cand_loops:
1477 if l[uv_layer].select and l.link_loop_next[uv_layer].select:
1478 d = {l, l.link_loop_next}
1479 if d not in target_loop_pairs:
1480 assert l.face == l.link_loop_next.face
1481 target_loop_pairs.append(d)
1483 return target_loop_pairs
1485 def execute(self, context):
1486 sc = context.scene
1487 objs = common.get_uv_editable_objects(context)
1489 ave_uv_1 = Vector((0.0, 0.0))
1490 ave_uv_2 = Vector((0.0, 0.0))
1491 count = 0
1492 for obj in objs:
1493 bm = bmesh.from_edit_mesh(obj.data)
1494 if common.check_version(2, 73, 0) >= 0:
1495 bm.faces.ensure_lookup_table()
1496 uv_layer = bm.loops.layers.uv.verify()
1498 target_loop_pairs = self._get_target_loop_pairs(bm, uv_layer)
1499 for pair in target_loop_pairs:
1500 p = list(pair)
1501 uv_1 = p[0][uv_layer].uv
1502 uv_2 = p[1][uv_layer].uv
1503 ave_uv_1 += uv_1
1504 ave_uv_2 += uv_2
1505 count += 1
1507 if count != 0:
1508 ave_uv_1 /= count
1509 ave_uv_2 /= count
1511 sc.muv_align_uv_snap_edge_target_1 = ave_uv_1
1512 sc.muv_align_uv_snap_edge_target_2 = ave_uv_2
1514 return {'FINISHED'}