Cleanup: Node Wrangler: preview_node operator
[blender-addons.git] / magic_uv / op / align_uv.py
blob58bd2da7d2fa97b2547469c812905a694044cbf7
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, tan, sin, cos
13 import bpy
14 from bpy.props import (
15 EnumProperty,
16 BoolProperty,
17 FloatProperty,
18 FloatVectorProperty,
20 import bmesh
21 from mathutils import Vector
23 from ..utils.bl_class_registry import BlClassRegistry
24 from ..utils.property_class_registry import PropertyClassRegistry
25 from ..utils import compatibility as compat
27 from .. import common
30 def _is_valid_context(context):
31 # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
32 # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
33 # after the execution
34 if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']):
35 return False
37 objs = common.get_uv_editable_objects(context)
38 if not objs:
39 return False
41 # only edit mode is allowed to execute
42 if context.object.mode != 'EDIT':
43 return False
45 return True
48 # get sum vertex length of loop sequences
49 def _get_loop_vert_len(loops):
50 length = 0
51 for l1, l2 in zip(loops[:-1], loops[1:]):
52 diff = l2.vert.co - l1.vert.co
53 length = length + abs(diff.length)
55 return length
58 # get sum uv length of loop sequences
59 def _get_loop_uv_len(loops, uv_layer):
60 length = 0
61 for l1, l2 in zip(loops[:-1], loops[1:]):
62 diff = l2[uv_layer].uv - l1[uv_layer].uv
63 length = length + abs(diff.length)
65 return length
68 # get center/radius of circle by 3 vertices
69 def _get_circle(v):
70 alpha = atan2((v[0].y - v[1].y), (v[0].x - v[1].x)) + math.pi / 2
71 beta = atan2((v[1].y - v[2].y), (v[1].x - v[2].x)) + math.pi / 2
72 ex = (v[0].x + v[1].x) / 2.0
73 ey = (v[0].y + v[1].y) / 2.0
74 fx = (v[1].x + v[2].x) / 2.0
75 fy = (v[1].y + v[2].y) / 2.0
76 cx = (ey - fy - ex * tan(alpha) + fx * tan(beta)) / \
77 (tan(beta) - tan(alpha))
78 cy = ey - (ex - cx) * tan(alpha)
79 center = Vector((cx, cy))
81 r = v[0] - center
82 radian = r.length
84 return center, radian
87 # get position on circle with same arc length
88 def _calc_v_on_circle(v, center, radius):
89 base = v[0]
90 theta = atan2(base.y - center.y, base.x - center.x)
91 new_v = []
92 for i in range(len(v)):
93 angle = theta + i * 2 * math.pi / len(v)
94 new_v.append(Vector((center.x + radius * sin(angle),
95 center.y + radius * cos(angle))))
97 return new_v
100 # get accumulate vertex lengths of loop sequences
101 def _get_loop_vert_accum_len(loops):
102 accum_lengths = [0.0]
103 length = 0
104 for l1, l2 in zip(loops[:-1], loops[1:]):
105 diff = l2.vert.co - l1.vert.co
106 length = length + abs(diff.length)
107 accum_lengths.extend([length])
109 return accum_lengths
112 # get sum uv length of loop sequences
113 def _get_loop_uv_accum_len(loops, uv_layer):
114 accum_lengths = [0.0]
115 length = 0
116 for l1, l2 in zip(loops[:-1], loops[1:]):
117 diff = l2[uv_layer].uv - l1[uv_layer].uv
118 length = length + abs(diff.length)
119 accum_lengths.extend([length])
121 return accum_lengths
124 # get horizontal differential of UV influenced by mesh vertex
125 def _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl):
126 common.debug_print(
127 "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx))
129 base_uv = loop_seqs[0][vidx][0][uv_layer].uv.copy()
131 # calculate original length
132 hloops = []
133 for s in loop_seqs:
134 hloops.extend([s[vidx][0], s[vidx][1]])
135 total_vlen = _get_loop_vert_len(hloops)
136 accum_vlens = _get_loop_vert_accum_len(hloops)
137 total_uvlen = _get_loop_uv_len(hloops, uv_layer)
138 accum_uvlens = _get_loop_uv_accum_len(hloops, uv_layer)
139 orig_uvs = [l[uv_layer].uv.copy() for l in hloops]
141 # calculate target length
142 tgt_noinfl = total_uvlen * (hidx + pidx) / len(loop_seqs)
143 tgt_infl = total_uvlen * accum_vlens[hidx * 2 + pidx] / total_vlen
144 target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl
145 common.debug_print(target_length)
146 common.debug_print(accum_uvlens)
148 # calculate target UV
149 for i in range(len(accum_uvlens[:-1])):
150 # get line segment which UV will be placed
151 if accum_uvlens[i] <= target_length < accum_uvlens[i + 1]:
152 tgt_seg_len = target_length - accum_uvlens[i]
153 seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
154 uv1 = orig_uvs[i]
155 uv2 = orig_uvs[i + 1]
156 target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
157 break
158 elif i == (len(accum_uvlens[:-1]) - 1):
159 if abs(accum_uvlens[i + 1] - target_length) > 0.000001:
160 raise Exception(
161 "Internal Error: horizontal_target_length={}"
162 " is not equal to {}"
163 .format(target_length, accum_uvlens[-1]))
164 tgt_seg_len = target_length - accum_uvlens[i]
165 seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
166 uv1 = orig_uvs[i]
167 uv2 = orig_uvs[i + 1]
168 target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
169 break
170 else:
171 raise Exception("Internal Error: horizontal_target_length={}"
172 " is not in range {} to {}"
173 .format(target_length, accum_uvlens[0],
174 accum_uvlens[-1]))
176 return target_uv
179 # --------------------- LOOP STRUCTURE ----------------------
181 # loops[hidx][vidx][pidx]
182 # hidx: horizontal index
183 # vidx: vertical index
184 # pidx: pair index
186 # <----- horizontal ----->
188 # (hidx, vidx, pidx) = (0, 3, 0)
189 # | (hidx, vidx, pidx) = (1, 3, 0)
190 # v v
191 # ^ o --- oo --- o
192 # | | || |
193 # vertical | o --- oo --- o <- (hidx, vidx, pidx)
194 # | o --- oo --- o = (1, 2, 1)
195 # | | || |
196 # v o --- oo --- o
197 # ^ ^
198 # | (hidx, vidx, pidx) = (1, 0, 1)
199 # (hidx, vidx, pidx) = (0, 0, 0)
201 # -----------------------------------------------------------
204 # get vertical differential of UV influenced by mesh vertex
205 def _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl):
206 common.debug_print(
207 "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx))
209 base_uv = loop_seqs[hidx][0][pidx][uv_layer].uv.copy()
211 # calculate original length
212 vloops = []
213 for s in loop_seqs[hidx]:
214 vloops.append(s[pidx])
215 total_vlen = _get_loop_vert_len(vloops)
216 accum_vlens = _get_loop_vert_accum_len(vloops)
217 total_uvlen = _get_loop_uv_len(vloops, uv_layer)
218 accum_uvlens = _get_loop_uv_accum_len(vloops, uv_layer)
219 orig_uvs = [l[uv_layer].uv.copy() for l in vloops]
221 # calculate target length
222 tgt_noinfl = total_uvlen * int((vidx + 1) / 2) * 2 / len(loop_seqs[hidx])
223 tgt_infl = total_uvlen * accum_vlens[vidx] / total_vlen
224 target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl
225 common.debug_print(target_length)
226 common.debug_print(accum_uvlens)
228 # calculate target UV
229 for i in range(len(accum_uvlens[:-1])):
230 # get line segment which UV will be placed
231 if accum_uvlens[i] <= target_length < accum_uvlens[i + 1]:
232 tgt_seg_len = target_length - accum_uvlens[i]
233 seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
234 uv1 = orig_uvs[i]
235 uv2 = orig_uvs[i + 1]
236 target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
237 break
238 elif i == (len(accum_uvlens[:-1]) - 1):
239 if abs(accum_uvlens[i + 1] - target_length) > 0.000001:
240 raise Exception("Internal Error: horizontal_target_length={}"
241 " is not equal to {}"
242 .format(target_length, accum_uvlens[-1]))
243 tgt_seg_len = target_length - accum_uvlens[i]
244 seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
245 uv1 = orig_uvs[i]
246 uv2 = orig_uvs[i + 1]
247 target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
248 break
249 else:
250 raise Exception("Internal Error: horizontal_target_length={}"
251 " is not in range {} to {}"
252 .format(target_length, accum_uvlens[0],
253 accum_uvlens[-1]))
255 return target_uv
258 # get horizontal differential of UV no influenced
259 def _get_hdiff_uv(uv_layer, loop_seqs, hidx):
260 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
261 h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv
263 return hidx * h_uv / len(loop_seqs)
266 # get vertical differential of UV no influenced
267 def _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx):
268 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
269 v_uv = loop_seqs[0][-1][0][uv_layer].uv.copy() - base_uv
271 hseq = loop_seqs[hidx]
272 return int((vidx + 1) / 2) * v_uv / (len(hseq) / 2)
275 @PropertyClassRegistry()
276 class _Properties:
277 idname = "align_uv"
279 @classmethod
280 def init_props(cls, scene):
281 scene.muv_align_uv_enabled = BoolProperty(
282 name="Align UV Enabled",
283 description="Align UV is enabled",
284 default=False
286 scene.muv_align_uv_transmission = BoolProperty(
287 name="Transmission",
288 description="Align linked UVs",
289 default=False
291 scene.muv_align_uv_select = BoolProperty(
292 name="Select",
293 description="Select UVs which are aligned",
294 default=False
296 scene.muv_align_uv_vertical = BoolProperty(
297 name="Vert-Infl (Vertical)",
298 description="Align vertical direction influenced "
299 "by mesh vertex proportion",
300 default=False
302 scene.muv_align_uv_horizontal = BoolProperty(
303 name="Vert-Infl (Horizontal)",
304 description="Align horizontal direction influenced "
305 "by mesh vertex proportion",
306 default=False
308 scene.muv_align_uv_mesh_infl = FloatProperty(
309 name="Mesh Influence",
310 description="Influence rate of mesh vertex",
311 min=0.0,
312 max=1.0,
313 default=0.0
315 scene.muv_align_uv_location = EnumProperty(
316 name="Location",
317 description="Align location",
318 items=[
319 ('LEFT_TOP', "Left/Top", "Align to Left or Top"),
320 ('MIDDLE', "Middle", "Align to middle"),
321 ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom")
323 default='MIDDLE'
326 scene.muv_align_uv_snap_method = EnumProperty(
327 name="Snap Method",
328 description="Snap method",
329 items=[
330 ('POINT', "Point", "Snap to point"),
331 ('EDGE', "Edge", "Snap to edge"),
333 default='POINT'
335 scene.muv_align_uv_snap_point_group = EnumProperty(
336 name="Snap Group (Point)",
337 description="Group that snap (point) operation applies for",
338 items=[
339 ('VERT', "Vertex", "Vertex"),
340 ('FACE', "Face", "Face"),
341 ('UV_ISLAND', "UV Island", "UV Island"),
343 default='FACE'
345 scene.muv_align_uv_snap_point_target = FloatVectorProperty(
346 name="Snap Target (Point)",
347 description="Target point where UV vertices snap to",
348 size=2,
349 precision=4,
350 soft_min=-1.0,
351 soft_max=1.0,
352 step=1,
353 default=(0.000, 0.000),
355 scene.muv_align_uv_snap_edge_group = EnumProperty(
356 name="Snap Group (Edge)",
357 description="Group that snap (edge) operation applies for",
358 items=[
359 ('EDGE', "Edge", "Edge"),
360 ('FACE', "Face", "Face"),
361 ('UV_ISLAND', "UV Island", "UV Island"),
363 default='FACE'
365 scene.muv_align_uv_snap_edge_target_1 = FloatVectorProperty(
366 name="Snap Target (Edge)",
367 description="Target edge where UV vertices snap to",
368 size=2,
369 precision=4,
370 soft_min=-1.0,
371 soft_max=1.0,
372 step=1,
373 default=(0.000, 0.000),
375 scene.muv_align_uv_snap_edge_target_2 = FloatVectorProperty(
376 name="Snap Target (Edge)",
377 description="Target edge where UV vertices snap to",
378 size=2,
379 precision=4,
380 soft_min=-1.0,
381 soft_max=1.0,
382 step=1,
383 default=(0.000, 0.000),
386 @classmethod
387 def del_props(cls, scene):
388 del scene.muv_align_uv_enabled
389 del scene.muv_align_uv_transmission
390 del scene.muv_align_uv_select
391 del scene.muv_align_uv_vertical
392 del scene.muv_align_uv_horizontal
393 del scene.muv_align_uv_mesh_infl
394 del scene.muv_align_uv_location
395 del scene.muv_align_uv_snap_method
396 del scene.muv_align_uv_snap_point_group
397 del scene.muv_align_uv_snap_point_target
398 del scene.muv_align_uv_snap_edge_group
399 del scene.muv_align_uv_snap_edge_target_1
400 del scene.muv_align_uv_snap_edge_target_2
403 @BlClassRegistry()
404 @compat.make_annotations
405 class MUV_OT_AlignUV_Circle(bpy.types.Operator):
407 bl_idname = "uv.muv_align_uv_circle"
408 bl_label = "Align UV (Circle)"
409 bl_description = "Align UV coordinates to Circle"
410 bl_options = {'REGISTER', 'UNDO'}
412 transmission = BoolProperty(
413 name="Transmission",
414 description="Align linked UVs",
415 default=False
417 select = BoolProperty(
418 name="Select",
419 description="Select UVs which are aligned",
420 default=False
423 @classmethod
424 def poll(cls, context):
425 # we can not get area/space/region from console
426 if common.is_console_mode():
427 return True
428 return _is_valid_context(context)
430 def execute(self, context):
431 objs = common.get_uv_editable_objects(context)
433 for obj in objs:
434 bm = bmesh.from_edit_mesh(obj.data)
435 if common.check_version(2, 73, 0) >= 0:
436 bm.faces.ensure_lookup_table()
437 uv_layer = bm.loops.layers.uv.verify()
439 # loop_seqs[horizontal][vertical][loop]
440 loop_seqs, error = common.get_loop_sequences(bm, uv_layer, True)
441 if not loop_seqs:
442 self.report({'WARNING'},
443 "Object {}: {}".format(obj.name, error))
444 return {'CANCELLED'}
446 # get circle and new UVs
447 uvs = [hseq[0][0][uv_layer].uv.copy() for hseq in loop_seqs]
448 c, r = _get_circle(uvs[0:3])
449 new_uvs = _calc_v_on_circle(uvs, c, r)
451 # check if center is identical
452 center_is_identical = False
453 center = loop_seqs[0][-1][0].vert
454 if (len(loop_seqs[0][-1]) == 1) and \
455 loop_seqs[0][-1][0].vert == center:
456 center_is_identical = True
458 # check if topology is correct
459 if center_is_identical:
460 for hseq in loop_seqs[1:]:
461 if len(hseq[-1]) != 1:
462 self.report({'WARNING'},
463 "Object {}: Last face must be triangle"
464 .format(obj.name))
465 return {'CANCELLED'}
466 if hseq[-1][0].vert != center:
467 self.report({'WARNING'},
468 "Object {}: Center must be identical"
469 .format(obj.name))
470 return {'CANCELLED'}
471 else:
472 for hseq in loop_seqs[1:]:
473 if len(hseq[-1]) == 1:
474 self.report({'WARNING'},
475 "Object {}: Last face must not be triangle"
476 .format(obj.name))
477 return {'CANCELLED'}
478 if hseq[-1][0].vert == center:
479 self.report({'WARNING'},
480 "Object {}: Center must not be identical"
481 .format(obj.name))
482 return {'CANCELLED'}
484 # align to circle
485 if self.transmission:
486 for hidx, hseq in enumerate(loop_seqs):
487 for vidx, pair in enumerate(hseq):
488 all_ = int((len(hseq) + 1) / 2)
489 if center_is_identical:
490 r = (all_ - int((vidx + 1) / 2)) / all_
491 else:
492 r = (1 + all_ - int((vidx + 1) / 2)) / all_
493 pair[0][uv_layer].uv = c + (new_uvs[hidx] - c) * r
494 if self.select:
495 pair[0][uv_layer].select = True
497 if len(pair) < 2:
498 continue
499 # for quad polygon
500 next_hidx = (hidx + 1) % len(loop_seqs)
501 pair[1][uv_layer].uv = \
502 c + ((new_uvs[next_hidx]) - c) * r
503 if self.select:
504 pair[1][uv_layer].select = True
505 else:
506 for hidx, hseq in enumerate(loop_seqs):
507 pair = hseq[0]
508 pair[0][uv_layer].uv = new_uvs[hidx]
509 pair[1][uv_layer].uv = new_uvs[(hidx + 1) % len(loop_seqs)]
510 if self.select:
511 pair[0][uv_layer].select = True
512 pair[1][uv_layer].select = True
514 bmesh.update_edit_mesh(obj.data)
516 return {'FINISHED'}
519 @BlClassRegistry()
520 @compat.make_annotations
521 class MUV_OT_AlignUV_Straighten(bpy.types.Operator):
523 bl_idname = "uv.muv_align_uv_straighten"
524 bl_label = "Align UV (Straighten)"
525 bl_description = "Straighten UV coordinates"
526 bl_options = {'REGISTER', 'UNDO'}
528 transmission = BoolProperty(
529 name="Transmission",
530 description="Align linked UVs",
531 default=False
533 select = BoolProperty(
534 name="Select",
535 description="Select UVs which are aligned",
536 default=False
538 vertical = BoolProperty(
539 name="Vert-Infl (Vertical)",
540 description="Align vertical direction influenced "
541 "by mesh vertex proportion",
542 default=False
544 horizontal = BoolProperty(
545 name="Vert-Infl (Horizontal)",
546 description="Align horizontal direction influenced "
547 "by mesh vertex proportion",
548 default=False
550 mesh_infl = FloatProperty(
551 name="Mesh Influence",
552 description="Influence rate of mesh vertex",
553 min=0.0,
554 max=1.0,
555 default=0.0
558 @classmethod
559 def poll(cls, context):
560 # we can not get area/space/region from console
561 if common.is_console_mode():
562 return True
563 return _is_valid_context(context)
565 # selected and paralleled UV loop sequence will be aligned
566 def __align_w_transmission(self, loop_seqs, uv_layer):
567 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
569 # calculate diff UVs
570 diff_uvs = []
571 # hseq[vertical][loop]
572 for hidx, hseq in enumerate(loop_seqs):
573 # pair[loop]
574 diffs = []
575 for vidx in range(0, len(hseq), 2):
576 if self.horizontal:
577 hdiff_uvs = [
578 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
579 self.mesh_infl),
580 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
581 self.mesh_infl),
582 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
583 hidx, 0, self.mesh_infl),
584 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
585 hidx, 1, self.mesh_infl),
587 else:
588 hdiff_uvs = [
589 _get_hdiff_uv(uv_layer, loop_seqs, hidx),
590 _get_hdiff_uv(uv_layer, loop_seqs, hidx + 1),
591 _get_hdiff_uv(uv_layer, loop_seqs, hidx),
592 _get_hdiff_uv(uv_layer, loop_seqs, hidx + 1)
594 if self.vertical:
595 vdiff_uvs = [
596 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
597 self.mesh_infl),
598 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
599 self.mesh_infl),
600 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
601 hidx, 0, self.mesh_infl),
602 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
603 hidx, 1, self.mesh_infl),
605 else:
606 vdiff_uvs = [
607 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
608 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
609 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
610 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
612 diffs.append([hdiff_uvs, vdiff_uvs])
613 diff_uvs.append(diffs)
615 # update UV
616 for hseq, diffs in zip(loop_seqs, diff_uvs):
617 for vidx in range(0, len(hseq), 2):
618 loops = [
619 hseq[vidx][0], hseq[vidx][1],
620 hseq[vidx + 1][0], hseq[vidx + 1][1]
622 for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
623 diffs[int(vidx / 2)][1]):
624 l[uv_layer].uv = base_uv + hdiff + vdiff
625 if self.select:
626 l[uv_layer].select = True
628 # only selected UV loop sequence will be aligned
629 def __align_wo_transmission(self, loop_seqs, uv_layer):
630 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
632 h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv
633 for hidx, hseq in enumerate(loop_seqs):
634 # only selected loop pair is targeted
635 pair = hseq[0]
636 hdiff_uv_0 = hidx * h_uv / len(loop_seqs)
637 hdiff_uv_1 = (hidx + 1) * h_uv / len(loop_seqs)
638 pair[0][uv_layer].uv = base_uv + hdiff_uv_0
639 pair[1][uv_layer].uv = base_uv + hdiff_uv_1
640 if self.select:
641 pair[0][uv_layer].select = True
642 pair[1][uv_layer].select = True
644 def __align(self, loop_seqs, uv_layer):
645 if self.transmission:
646 self.__align_w_transmission(loop_seqs, uv_layer)
647 else:
648 self.__align_wo_transmission(loop_seqs, uv_layer)
650 def execute(self, context):
651 objs = common.get_uv_editable_objects(context)
653 for obj in objs:
654 bm = bmesh.from_edit_mesh(obj.data)
655 if common.check_version(2, 73, 0) >= 0:
656 bm.faces.ensure_lookup_table()
657 uv_layer = bm.loops.layers.uv.verify()
659 # loop_seqs[horizontal][vertical][loop]
660 loop_seqs, error = common.get_loop_sequences(bm, uv_layer)
661 if not loop_seqs:
662 self.report({'WARNING'},
663 "Object {}: {}".format(obj.name, error))
664 return {'CANCELLED'}
666 # align
667 self.__align(loop_seqs, uv_layer)
669 bmesh.update_edit_mesh(obj.data)
671 return {'FINISHED'}
674 @BlClassRegistry()
675 @compat.make_annotations
676 class MUV_OT_AlignUV_Axis(bpy.types.Operator):
678 bl_idname = "uv.muv_align_uv_axis"
679 bl_label = "Align UV (XY-Axis)"
680 bl_description = "Align UV to XY-axis"
681 bl_options = {'REGISTER', 'UNDO'}
683 transmission = BoolProperty(
684 name="Transmission",
685 description="Align linked UVs",
686 default=False
688 select = BoolProperty(
689 name="Select",
690 description="Select UVs which are aligned",
691 default=False
693 vertical = BoolProperty(
694 name="Vert-Infl (Vertical)",
695 description="Align vertical direction influenced "
696 "by mesh vertex proportion",
697 default=False
699 horizontal = BoolProperty(
700 name="Vert-Infl (Horizontal)",
701 description="Align horizontal direction influenced "
702 "by mesh vertex proportion",
703 default=False
705 location = EnumProperty(
706 name="Location",
707 description="Align location",
708 items=[
709 ('LEFT_TOP', "Left/Top", "Align to Left or Top"),
710 ('MIDDLE', "Middle", "Align to middle"),
711 ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom")
713 default='MIDDLE'
715 mesh_infl = FloatProperty(
716 name="Mesh Influence",
717 description="Influence rate of mesh vertex",
718 min=0.0,
719 max=1.0,
720 default=0.0
723 @classmethod
724 def poll(cls, context):
725 # we can not get area/space/region from console
726 if common.is_console_mode():
727 return True
728 return _is_valid_context(context)
730 # get min/max of UV
731 def __get_uv_max_min(self, loop_seqs, uv_layer):
732 uv_max = Vector((-1000000.0, -1000000.0))
733 uv_min = Vector((1000000.0, 1000000.0))
734 for hseq in loop_seqs:
735 for l in hseq[0]:
736 uv = l[uv_layer].uv
737 uv_max.x = max(uv.x, uv_max.x)
738 uv_max.y = max(uv.y, uv_max.y)
739 uv_min.x = min(uv.x, uv_min.x)
740 uv_min.y = min(uv.y, uv_min.y)
742 return uv_max, uv_min
744 # get UV differentiation when UVs are aligned to X-axis
745 def __get_x_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min,
746 width, height):
747 diff_uvs = []
748 for hidx, hseq in enumerate(loop_seqs):
749 pair = hseq[0]
750 luv0 = pair[0][uv_layer]
751 luv1 = pair[1][uv_layer]
752 target_uv0 = Vector((0.0, 0.0))
753 target_uv1 = Vector((0.0, 0.0))
754 if self.location == 'RIGHT_BOTTOM':
755 target_uv0.y = target_uv1.y = uv_min.y
756 elif self.location == 'MIDDLE':
757 target_uv0.y = target_uv1.y = uv_min.y + height * 0.5
758 elif self.location == 'LEFT_TOP':
759 target_uv0.y = target_uv1.y = uv_min.y + height
760 if luv0.uv.x < luv1.uv.x:
761 target_uv0.x = uv_min.x + hidx * width / len(loop_seqs)
762 target_uv1.x = uv_min.x + (hidx + 1) * width / len(loop_seqs)
763 else:
764 target_uv0.x = uv_min.x + (hidx + 1) * width / len(loop_seqs)
765 target_uv1.x = uv_min.x + hidx * width / len(loop_seqs)
766 diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv])
768 return diff_uvs
770 # get UV differentiation when UVs are aligned to Y-axis
771 def __get_y_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min,
772 width, height):
773 diff_uvs = []
774 for hidx, hseq in enumerate(loop_seqs):
775 pair = hseq[0]
776 luv0 = pair[0][uv_layer]
777 luv1 = pair[1][uv_layer]
778 target_uv0 = Vector((0.0, 0.0))
779 target_uv1 = Vector((0.0, 0.0))
780 if self.location == 'RIGHT_BOTTOM':
781 target_uv0.x = target_uv1.x = uv_min.x + width
782 elif self.location == 'MIDDLE':
783 target_uv0.x = target_uv1.x = uv_min.x + width * 0.5
784 elif self.location == 'LEFT_TOP':
785 target_uv0.x = target_uv1.x = uv_min.x
786 if luv0.uv.y < luv1.uv.y:
787 target_uv0.y = uv_min.y + hidx * height / len(loop_seqs)
788 target_uv1.y = uv_min.y + (hidx + 1) * height / len(loop_seqs)
789 else:
790 target_uv0.y = uv_min.y + (hidx + 1) * height / len(loop_seqs)
791 target_uv1.y = uv_min.y + hidx * height / len(loop_seqs)
792 diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv])
794 return diff_uvs
796 # only selected UV loop sequence will be aligned along to X-axis
797 def __align_to_x_axis_wo_transmission(self, loop_seqs, uv_layer,
798 uv_min, width, height):
799 # reverse if the UV coordinate is not sorted by position
800 need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \
801 loop_seqs[-1][0][0][uv_layer].uv.x
802 if need_revese:
803 loop_seqs.reverse()
804 for hidx, hseq in enumerate(loop_seqs):
805 for vidx, pair in enumerate(hseq):
806 tmp = loop_seqs[hidx][vidx][0]
807 loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
808 loop_seqs[hidx][vidx][1] = tmp
810 # get UV differential
811 diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs,
812 uv_layer, uv_min,
813 width, height)
815 # update UV
816 for hseq, duv in zip(loop_seqs, diff_uvs):
817 pair = hseq[0]
818 luv0 = pair[0][uv_layer]
819 luv1 = pair[1][uv_layer]
820 luv0.uv = luv0.uv + duv[0]
821 luv1.uv = luv1.uv + duv[1]
823 # only selected UV loop sequence will be aligned along to Y-axis
824 def __align_to_y_axis_wo_transmission(self, loop_seqs, uv_layer,
825 uv_min, width, height):
826 # reverse if the UV coordinate is not sorted by position
827 need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \
828 loop_seqs[-1][0][0][uv_layer].uv.y
829 if need_revese:
830 loop_seqs.reverse()
831 for hidx, hseq in enumerate(loop_seqs):
832 for vidx, pair in enumerate(hseq):
833 tmp = loop_seqs[hidx][vidx][0]
834 loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
835 loop_seqs[hidx][vidx][1] = tmp
837 # get UV differential
838 diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs,
839 uv_layer, uv_min,
840 width, height)
842 # update UV
843 for hseq, duv in zip(loop_seqs, diff_uvs):
844 pair = hseq[0]
845 luv0 = pair[0][uv_layer]
846 luv1 = pair[1][uv_layer]
847 luv0.uv = luv0.uv + duv[0]
848 luv1.uv = luv1.uv + duv[1]
850 # selected and paralleled UV loop sequence will be aligned along to X-axis
851 def __align_to_x_axis_w_transmission(self, loop_seqs, uv_layer,
852 uv_min, width, height):
853 # reverse if the UV coordinate is not sorted by position
854 need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \
855 loop_seqs[-1][0][0][uv_layer].uv.x
856 if need_revese:
857 loop_seqs.reverse()
858 for hidx, hseq in enumerate(loop_seqs):
859 for vidx in range(len(hseq)):
860 tmp = loop_seqs[hidx][vidx][0]
861 loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
862 loop_seqs[hidx][vidx][1] = tmp
864 # get offset UVs when the UVs are aligned to X-axis
865 align_diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs,
866 uv_layer, uv_min,
867 width, height)
868 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
869 offset_uvs = []
870 for hseq, aduv in zip(loop_seqs, align_diff_uvs):
871 luv0 = hseq[0][0][uv_layer]
872 luv1 = hseq[0][1][uv_layer]
873 offset_uvs.append([luv0.uv + aduv[0] - base_uv,
874 luv1.uv + aduv[1] - base_uv])
876 # get UV differential
877 diff_uvs = []
878 # hseq[vertical][loop]
879 for hidx, hseq in enumerate(loop_seqs):
880 # pair[loop]
881 diffs = []
882 for vidx in range(0, len(hseq), 2):
883 if self.horizontal:
884 hdiff_uvs = [
885 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
886 self.mesh_infl),
887 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
888 self.mesh_infl),
889 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
890 hidx, 0, self.mesh_infl),
891 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
892 hidx, 1, self.mesh_infl),
894 hdiff_uvs[0].y = hdiff_uvs[0].y + offset_uvs[hidx][0].y
895 hdiff_uvs[1].y = hdiff_uvs[1].y + offset_uvs[hidx][1].y
896 hdiff_uvs[2].y = hdiff_uvs[2].y + offset_uvs[hidx][0].y
897 hdiff_uvs[3].y = hdiff_uvs[3].y + offset_uvs[hidx][1].y
898 else:
899 hdiff_uvs = [
900 offset_uvs[hidx][0],
901 offset_uvs[hidx][1],
902 offset_uvs[hidx][0],
903 offset_uvs[hidx][1],
905 if self.vertical:
906 vdiff_uvs = [
907 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
908 self.mesh_infl),
909 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
910 self.mesh_infl),
911 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
912 hidx, 0, self.mesh_infl),
913 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
914 hidx, 1, self.mesh_infl),
916 else:
917 vdiff_uvs = [
918 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
919 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
920 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
921 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
923 diffs.append([hdiff_uvs, vdiff_uvs])
924 diff_uvs.append(diffs)
926 # update UV
927 for hseq, diffs in zip(loop_seqs, diff_uvs):
928 for vidx in range(0, len(hseq), 2):
929 loops = [
930 hseq[vidx][0], hseq[vidx][1],
931 hseq[vidx + 1][0], hseq[vidx + 1][1]
933 for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
934 diffs[int(vidx / 2)][1]):
935 l[uv_layer].uv = base_uv + hdiff + vdiff
936 if self.select:
937 l[uv_layer].select = True
939 # selected and paralleled UV loop sequence will be aligned along to Y-axis
940 def __align_to_y_axis_w_transmission(self, loop_seqs, uv_layer,
941 uv_min, width, height):
942 # reverse if the UV coordinate is not sorted by position
943 need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \
944 loop_seqs[-1][0][-1][uv_layer].uv.y
945 if need_revese:
946 loop_seqs.reverse()
947 for hidx, hseq in enumerate(loop_seqs):
948 for vidx in range(len(hseq)):
949 tmp = loop_seqs[hidx][vidx][0]
950 loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
951 loop_seqs[hidx][vidx][1] = tmp
953 # get offset UVs when the UVs are aligned to Y-axis
954 align_diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs,
955 uv_layer, uv_min,
956 width, height)
957 base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
958 offset_uvs = []
959 for hseq, aduv in zip(loop_seqs, align_diff_uvs):
960 luv0 = hseq[0][0][uv_layer]
961 luv1 = hseq[0][1][uv_layer]
962 offset_uvs.append([luv0.uv + aduv[0] - base_uv,
963 luv1.uv + aduv[1] - base_uv])
965 # get UV differential
966 diff_uvs = []
967 # hseq[vertical][loop]
968 for hidx, hseq in enumerate(loop_seqs):
969 # pair[loop]
970 diffs = []
971 for vidx in range(0, len(hseq), 2):
972 if self.horizontal:
973 hdiff_uvs = [
974 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
975 self.mesh_infl),
976 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
977 self.mesh_infl),
978 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
979 hidx, 0, self.mesh_infl),
980 _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
981 hidx, 1, self.mesh_infl),
983 hdiff_uvs[0].x = hdiff_uvs[0].x + offset_uvs[hidx][0].x
984 hdiff_uvs[1].x = hdiff_uvs[1].x + offset_uvs[hidx][1].x
985 hdiff_uvs[2].x = hdiff_uvs[2].x + offset_uvs[hidx][0].x
986 hdiff_uvs[3].x = hdiff_uvs[3].x + offset_uvs[hidx][1].x
987 else:
988 hdiff_uvs = [
989 offset_uvs[hidx][0],
990 offset_uvs[hidx][1],
991 offset_uvs[hidx][0],
992 offset_uvs[hidx][1],
994 if self.vertical:
995 vdiff_uvs = [
996 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
997 self.mesh_infl),
998 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
999 self.mesh_infl),
1000 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
1001 hidx, 0, self.mesh_infl),
1002 _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
1003 hidx, 1, self.mesh_infl),
1005 else:
1006 vdiff_uvs = [
1007 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
1008 _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
1009 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
1010 _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
1012 diffs.append([hdiff_uvs, vdiff_uvs])
1013 diff_uvs.append(diffs)
1015 # update UV
1016 for hseq, diffs in zip(loop_seqs, diff_uvs):
1017 for vidx in range(0, len(hseq), 2):
1018 loops = [
1019 hseq[vidx][0], hseq[vidx][1],
1020 hseq[vidx + 1][0], hseq[vidx + 1][1]
1022 for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
1023 diffs[int(vidx / 2)][1]):
1024 l[uv_layer].uv = base_uv + hdiff + vdiff
1025 if self.select:
1026 l[uv_layer].select = True
1028 def __align(self, loop_seqs, uv_layer, uv_min, width, height):
1029 # align along to x-axis
1030 if width > height:
1031 if self.transmission:
1032 self.__align_to_x_axis_w_transmission(loop_seqs,
1033 uv_layer, uv_min,
1034 width, height)
1035 else:
1036 self.__align_to_x_axis_wo_transmission(loop_seqs,
1037 uv_layer, uv_min,
1038 width, height)
1039 # align along to y-axis
1040 else:
1041 if self.transmission:
1042 self.__align_to_y_axis_w_transmission(loop_seqs,
1043 uv_layer, uv_min,
1044 width, height)
1045 else:
1046 self.__align_to_y_axis_wo_transmission(loop_seqs,
1047 uv_layer, uv_min,
1048 width, height)
1050 def execute(self, context):
1051 objs = common.get_uv_editable_objects(context)
1053 for obj in objs:
1054 bm = bmesh.from_edit_mesh(obj.data)
1055 if common.check_version(2, 73, 0) >= 0:
1056 bm.faces.ensure_lookup_table()
1057 uv_layer = bm.loops.layers.uv.verify()
1059 # loop_seqs[horizontal][vertical][loop]
1060 loop_seqs, error = common.get_loop_sequences(bm, uv_layer)
1061 if not loop_seqs:
1062 self.report({'WARNING'},
1063 "Object {}: {}".format(obj.name, error))
1064 return {'CANCELLED'}
1066 # get height and width
1067 uv_max, uv_min = self.__get_uv_max_min(loop_seqs, uv_layer)
1068 width = uv_max.x - uv_min.x
1069 height = uv_max.y - uv_min.y
1071 self.__align(loop_seqs, uv_layer, uv_min, width, height)
1073 bmesh.update_edit_mesh(obj.data)
1075 return {'FINISHED'}
1078 @BlClassRegistry()
1079 @compat.make_annotations
1080 class MUV_OT_AlignUV_SnapToPoint(bpy.types.Operator):
1082 bl_idname = "uv.muv_align_uv_snap_to_point"
1083 bl_label = "Align UV (Snap to Point)"
1084 bl_description = "Align UV to the target point"
1085 bl_options = {'REGISTER', 'UNDO'}
1087 group = EnumProperty(
1088 name="Snap Group",
1089 description="Group that snap operation applies for",
1090 items=[
1091 ('VERT', "Vertex", "Vertex"),
1092 ('FACE', "Face", "Face"),
1093 ('UV_ISLAND', "UV Island", "UV Island"),
1095 default='FACE'
1097 target = FloatVectorProperty(
1098 name="Snap Target",
1099 description="Target where UV vertices snap to",
1100 size=2,
1101 precision=4,
1102 soft_min=-1.0,
1103 soft_max=1.0,
1104 step=1,
1105 default=(0.000, 0.000),
1108 def _get_snap_target_loops(self, context, bm, uv_layer):
1109 target_loops = []
1111 selected_faces = [f for f in bm.faces if f.select]
1113 # Process snap operation.
1114 for face in selected_faces:
1115 for l in face.loops:
1116 if context.tool_settings.use_uv_select_sync or \
1117 l[uv_layer].select:
1118 target_loops.append(l)
1120 return target_loops
1122 def _get_snap_target_faces(self, context, bm, uv_layer):
1123 target_faces = []
1125 selected_faces = [f for f in bm.faces if f.select]
1127 for face in selected_faces:
1128 for l in face.loops:
1129 if not context.tool_settings.use_uv_select_sync and \
1130 not l[uv_layer].select:
1131 break
1132 else:
1133 target_faces.append(face)
1135 return target_faces
1137 def _get_snap_target_islands(self, context, bm, uv_layer):
1138 target_islands = []
1140 islands = common.get_island_info_from_bmesh(bm, only_selected=True)
1142 for isl in islands:
1143 some_verts_not_selected = False
1144 for face in isl["faces"]:
1145 for l in face["face"].loops:
1146 if not context.tool_settings.use_uv_select_sync and \
1147 not l[uv_layer].select:
1148 some_verts_not_selected = True
1149 break
1150 if not some_verts_not_selected:
1151 target_islands.append(isl)
1153 return target_islands
1155 def execute(self, context):
1156 objs = common.get_uv_editable_objects(context)
1158 group_to_reason = {
1159 'VERT': "Vertex",
1160 'FACE': "Face",
1161 'UV_ISLAND': "UV Island",
1163 no_selection_reason = group_to_reason[self.group]
1165 for obj in objs:
1166 bm = bmesh.from_edit_mesh(obj.data)
1167 if common.check_version(2, 73, 0) >= 0:
1168 bm.faces.ensure_lookup_table()
1169 uv_layer = bm.loops.layers.uv.verify()
1171 if self.group == 'VERT':
1172 target_loops = \
1173 self._get_snap_target_loops(context, bm, uv_layer)
1175 # Process snap operation.
1176 for l in target_loops:
1177 l[uv_layer].uv = self.target
1178 no_selection_reason = None
1180 elif self.group == 'FACE':
1181 target_faces = \
1182 self._get_snap_target_faces(context, bm, uv_layer)
1184 for face in target_faces:
1185 ave_uv = Vector((0.0, 0.0))
1186 for l in face.loops:
1187 ave_uv += l[uv_layer].uv
1188 ave_uv /= len(face.loops)
1189 diff = Vector(self.target) - ave_uv
1191 # Process snap operation.
1192 for l in face.loops:
1193 l[uv_layer].uv += diff
1194 no_selection_reason = None
1196 elif self.group == 'UV_ISLAND':
1197 target_islands = \
1198 self._get_snap_target_islands(context, bm, uv_layer)
1200 for isl in target_islands:
1201 ave_uv = Vector((0.0, 0.0))
1202 count = 0
1203 for face in isl["faces"]:
1204 for l in face["face"].loops:
1205 ave_uv += l[uv_layer].uv
1206 count += 1
1207 if count != 0:
1208 ave_uv /= count
1209 diff = Vector(self.target) - ave_uv
1211 # Process snap operation.
1212 for face in isl["faces"]:
1213 for l in face["face"].loops:
1214 l[uv_layer].uv += diff
1215 no_selection_reason = None
1217 bmesh.update_edit_mesh(obj.data)
1219 if no_selection_reason:
1220 self.report(
1221 {'WARNING'},
1222 "Must select more than 1 {}.".format(no_selection_reason)
1224 return {'CANCELLED'}
1226 return {'FINISHED'}
1229 @BlClassRegistry()
1230 @compat.make_annotations
1231 class MUV_OT_AlignUV_Snap_SetPointTargetToCursor(bpy.types.Operator):
1233 bl_idname = "uv.muv_align_uv_snap_set_point_target_to_cursor"
1234 bl_label = "Set Point Target to Cursor"
1235 bl_description = """Set point target to the cursor for
1236 'Align UV (Snap to Point)'"""
1237 bl_options = {'REGISTER', 'UNDO'}
1239 def execute(self, context):
1240 sc = context.scene
1242 _, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
1243 'IMAGE_EDITOR')
1244 cursor_loc = space.cursor_location
1246 sc.muv_align_uv_snap_point_target = cursor_loc
1248 return {'FINISHED'}
1251 @BlClassRegistry()
1252 @compat.make_annotations
1253 class MUV_OT_AlignUV_Snap_SetPointTargetToVertexGroup(bpy.types.Operator):
1255 bl_idname = "uv.muv_align_uv_snap_set_point_target_to_vertex_group"
1256 bl_label = "Set Point Target to Vertex Group"
1257 bl_description = """Set point target to the average of vertices for
1258 'Align UV (Snap to Point)'"""
1259 bl_options = {'REGISTER', 'UNDO'}
1261 def execute(self, context):
1262 sc = context.scene
1263 objs = common.get_uv_editable_objects(context)
1265 ave_uv = Vector((0.0, 0.0))
1266 count = 0
1267 for obj in objs:
1268 bm = bmesh.from_edit_mesh(obj.data)
1269 if common.check_version(2, 73, 0) >= 0:
1270 bm.faces.ensure_lookup_table()
1271 uv_layer = bm.loops.layers.uv.verify()
1273 selected_faces = [f for f in bm.faces if f.select]
1274 for face in selected_faces:
1275 for l in face.loops:
1276 if context.tool_settings.use_uv_select_sync or \
1277 l[uv_layer].select:
1278 ave_uv += l[uv_layer].uv
1279 count += 1
1280 if count != 0:
1281 ave_uv /= count
1283 sc.muv_align_uv_snap_point_target = ave_uv
1285 return {'FINISHED'}
1288 @BlClassRegistry()
1289 @compat.make_annotations
1290 class MUV_OT_AlignUV_SnapToEdge(bpy.types.Operator):
1292 bl_idname = "uv.muv_align_uv_snap_to_edge"
1293 bl_label = "Align UV (Snap to Edge)"
1294 bl_description = "Align UV to the target edge"
1295 bl_options = {'REGISTER', 'UNDO'}
1297 group = EnumProperty(
1298 name="Snap Group",
1299 description="Group that snap operation applies for",
1300 items=[
1301 ('EDGE', "Edge", "Edge"),
1302 ('FACE', "Face", "Face"),
1303 ('UV_ISLAND', "UV Island", "UV Island"),
1305 default='FACE'
1307 target_1 = FloatVectorProperty(
1308 name="Snap Target 1",
1309 description="Vertex 1 of the target edge",
1310 size=2,
1311 precision=4,
1312 soft_min=-1.0,
1313 soft_max=1.0,
1314 step=1,
1315 default=(0.000, 0.000),
1317 target_2 = FloatVectorProperty(
1318 name="Snap Target 2",
1319 description="Vertex 2 of the target edge",
1320 size=2,
1321 precision=4,
1322 soft_min=-1.0,
1323 soft_max=1.0,
1324 step=1,
1325 default=(0.000, 0.000),
1328 def _calc_snap_move_amount(self, loops, uv_layer):
1329 ave = (loops[0][uv_layer].uv + loops[1][uv_layer].uv) / 2
1330 target = (Vector(self.target_1) + Vector(self.target_2)) / 2
1332 return target - ave
1334 def _get_snap_target_loop_pairs(self, bm, uv_layer):
1335 target_loop_pairs = []
1337 selected_edges = [e for e in bm.edges if e.select]
1339 cand_loops = []
1340 for edge in selected_edges:
1341 for l in edge.link_loops:
1342 if l[uv_layer].select:
1343 cand_loops.append(l)
1345 for l in cand_loops:
1346 if l[uv_layer].select and l.link_loop_next[uv_layer].select:
1347 d = {l, l.link_loop_next}
1348 if d not in target_loop_pairs:
1349 assert l.face == l.link_loop_next.face
1350 target_loop_pairs.append(d)
1352 return target_loop_pairs
1354 def _find_target_island_from_face(self, islands, face):
1355 for isl in islands:
1356 for f in isl["faces"]:
1357 if f["face"] == face:
1358 return isl
1360 return None
1362 def execute(self, context):
1363 objs = common.get_uv_editable_objects(context)
1365 no_selection = True
1366 for obj in objs:
1367 bm = bmesh.from_edit_mesh(obj.data)
1368 if common.check_version(2, 73, 0) >= 0:
1369 bm.faces.ensure_lookup_table()
1370 uv_layer = bm.loops.layers.uv.verify()
1372 if self.group == 'EDGE':
1373 target_loop_pairs = \
1374 self._get_snap_target_loop_pairs(bm, uv_layer)
1376 for pair in target_loop_pairs:
1377 p = list(pair)
1378 diff = self._calc_snap_move_amount(p, uv_layer)
1380 # Process snap operation.
1381 p[0][uv_layer].uv += diff
1382 p[1][uv_layer].uv += diff
1384 no_selection = False
1386 elif self.group == 'FACE':
1387 target_loop_pairs = \
1388 self._get_snap_target_loop_pairs(bm, uv_layer)
1390 face_processed = []
1391 for pair in target_loop_pairs:
1392 p = list(pair)
1393 diff = self._calc_snap_move_amount(p, uv_layer)
1395 # Process snap operation.
1396 face = p[0].face
1397 if face in face_processed:
1398 self.report(
1399 {'WARNING'},
1400 "Must select only one edge per face. (Object: {})"
1401 .format(obj.name)
1403 return {'CANCELLED'}
1404 face_processed.append(face)
1405 for l in face.loops:
1406 l[uv_layer].uv += diff
1407 no_selection = False
1409 elif self.group == 'UV_ISLAND':
1410 target_loop_pairs = \
1411 self._get_snap_target_loop_pairs(bm, uv_layer)
1413 islands = common.get_island_info_from_bmesh(
1414 bm, only_selected=False)
1416 isl_processed = []
1417 for pair in target_loop_pairs:
1418 p = list(pair)
1419 diff = self._calc_snap_move_amount(p, uv_layer)
1421 # Find island to process.
1422 face = p[0].face
1423 target_isl = \
1424 self._find_target_island_from_face(islands, face)
1425 if target_isl is None:
1426 self.report(
1427 {'WARNING'},
1428 "Failed to find island. (Object: {})"
1429 .format(obj.name)
1431 return {'CANCELLED'}
1432 if target_isl in isl_processed:
1433 self.report(
1434 {'WARNING'},
1435 """Must select only one edge per island.
1436 (Object: {})"""
1437 .format(obj.name)
1439 return {'CANCELLED'}
1440 isl_processed.append(target_isl)
1442 # Process snap operation.
1443 for f in target_isl["faces"]:
1444 for l in f["face"].loops:
1445 l[uv_layer].uv += diff
1446 no_selection = False
1448 bmesh.update_edit_mesh(obj.data)
1450 if no_selection:
1451 self.report({'WARNING'}, "Must select more than 1 Edge.")
1452 return {'CANCELLED'}
1454 return {'FINISHED'}
1457 @BlClassRegistry()
1458 @compat.make_annotations
1459 class MUV_OT_AlignUV_Snap_SetEdgeTargetToEdgeCenter(bpy.types.Operator):
1461 bl_idname = "uv.muv_align_uv_snap_set_edge_target_to_edge_center"
1462 bl_label = "Set Edge Target to Edge Center"
1463 bl_description = """Set edge target to the center of edge for
1464 'Align UV (Snap to Edge)'"""
1465 bl_options = {'REGISTER', 'UNDO'}
1467 def _get_target_loop_pairs(self, bm, uv_layer):
1468 target_loop_pairs = []
1470 selected_edges = [e for e in bm.edges if e.select]
1472 cand_loops = []
1473 for edge in selected_edges:
1474 for l in edge.link_loops:
1475 if l[uv_layer].select:
1476 cand_loops.append(l)
1478 for l in cand_loops:
1479 if l[uv_layer].select and l.link_loop_next[uv_layer].select:
1480 d = {l, l.link_loop_next}
1481 if d not in target_loop_pairs:
1482 assert l.face == l.link_loop_next.face
1483 target_loop_pairs.append(d)
1485 return target_loop_pairs
1487 def execute(self, context):
1488 sc = context.scene
1489 objs = common.get_uv_editable_objects(context)
1491 ave_uv_1 = Vector((0.0, 0.0))
1492 ave_uv_2 = Vector((0.0, 0.0))
1493 count = 0
1494 for obj in objs:
1495 bm = bmesh.from_edit_mesh(obj.data)
1496 if common.check_version(2, 73, 0) >= 0:
1497 bm.faces.ensure_lookup_table()
1498 uv_layer = bm.loops.layers.uv.verify()
1500 target_loop_pairs = self._get_target_loop_pairs(bm, uv_layer)
1501 for pair in target_loop_pairs:
1502 p = list(pair)
1503 uv_1 = p[0][uv_layer].uv
1504 uv_2 = p[1][uv_layer].uv
1505 ave_uv_1 += uv_1
1506 ave_uv_2 += uv_2
1507 count += 1
1509 if count != 0:
1510 ave_uv_1 /= count
1511 ave_uv_2 /= count
1513 sc.muv_align_uv_snap_edge_target_1 = ave_uv_1
1514 sc.muv_align_uv_snap_edge_target_2 = ave_uv_2
1516 return {'FINISHED'}