1 # SPDX-FileCopyrightText: 2017-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 __author__
= "Nutti <nutti.metro@gmail.com>"
6 __status__
= "production"
8 __date__
= "9 Sep 2022"
10 from enum
import IntEnum
16 from bpy
.props
import BoolProperty
, EnumProperty
19 from ..utils
.bl_class_registry
import BlClassRegistry
20 from ..utils
.property_class_registry
import PropertyClassRegistry
21 from ..utils
import compatibility
as compat
24 from gpu_extras
.batch
import batch_for_shader
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
34 if not common
.is_valid_space(context
, ['IMAGE_EDITOR', 'VIEW_3D']):
39 # only edit mode is allowed to execute
42 if obj
.type != 'MESH':
44 if context
.object.mode
!= 'EDIT':
50 @PropertyClassRegistry()
52 idname
= "uv_bounding_box"
55 def init_props(cls
, scene
):
61 scene
.muv_props
.uv_bounding_box
= Props()
64 return MUV_OT_UVBoundingBox
.is_running(bpy
.context
)
69 def update_func(_
, __
):
70 bpy
.ops
.uv
.muv_uv_bounding_box('INVOKE_REGION_WIN')
72 scene
.muv_uv_bounding_box_enabled
= BoolProperty(
73 name
="UV Bounding Box Enabled",
74 description
="UV Bounding Box is enabled",
77 scene
.muv_uv_bounding_box_show
= BoolProperty(
78 name
="UV Bounding Box Showed",
79 description
="UV Bounding Box is showed",
85 scene
.muv_uv_bounding_box_uniform_scaling
= BoolProperty(
86 name
="Uniform Scaling",
87 description
="Enable Uniform Scaling",
90 scene
.muv_uv_bounding_box_boundary
= EnumProperty(
92 description
="Boundary",
95 ('UV', "UV", "Boundary is decided by UV"),
96 ('UV_SEL', "UV (Selected)",
97 "Boundary is decided by Selected UV")
102 def del_props(cls
, scene
):
103 del scene
.muv_props
.uv_bounding_box
104 del scene
.muv_uv_bounding_box_enabled
105 del scene
.muv_uv_bounding_box_show
106 del scene
.muv_uv_bounding_box_uniform_scaling
107 del scene
.muv_uv_bounding_box_boundary
112 Custom class: Base class of command
116 self
.op
= 'NONE' # operation
120 mat
= mathutils
.Matrix()
125 class TranslationCommand(CommandBase
):
127 Custom class: Translation operation
130 def __init__(self
, ix
, iy
):
132 self
.op
= 'TRANSLATION'
133 self
.__x
= ix
# current x
134 self
.__y
= iy
# current y
135 self
.__ix
= ix
# initial x
136 self
.__iy
= iy
# initial y
140 dx
= self
.__x
- self
.__ix
141 dy
= self
.__y
- self
.__iy
142 return mathutils
.Matrix
.Translation((dx
, dy
, 0))
149 class RotationCommand(CommandBase
):
151 Custom class: Rotation operation
154 def __init__(self
, ix
, iy
, cx
, cy
):
157 self
.__x
= ix
# current x
158 self
.__y
= iy
# current y
159 self
.__cx
= cx
# center of rotation x
160 self
.__cy
= cy
# center of rotation y
161 dx
= self
.__x
- self
.__cx
162 dy
= self
.__y
- self
.__cy
163 self
.__iangle
= math
.atan2(dy
, dx
) # initial rotation angle
166 # mat = Mt * Mr * Mt^-1
167 dx
= self
.__x
- self
.__cx
168 dy
= self
.__y
- self
.__cy
169 angle
= math
.atan2(dy
, dx
) - self
.__iangle
170 mti
= mathutils
.Matrix
.Translation((-self
.__cx
, -self
.__cy
, 0.0))
171 mr
= mathutils
.Matrix
.Rotation(angle
, 4, 'Z')
172 mt
= mathutils
.Matrix
.Translation((self
.__cx
, self
.__cy
, 0.0))
173 return compat
.matmul(compat
.matmul(mt
, mr
), mti
)
180 class ScalingCommand(CommandBase
):
182 Custom class: Scaling operation
185 def __init__(self
, ix
, iy
, ox
, oy
, dir_x
, dir_y
, mat
):
188 self
.__ix
= ix
# initial x
189 self
.__iy
= iy
# initial y
190 self
.__x
= ix
# current x
191 self
.__y
= iy
# current y
192 self
.__ox
= ox
# origin of scaling x
193 self
.__oy
= oy
# origin of scaling y
194 self
.__dir
_x
= dir_x
# direction of scaling x
195 self
.__dir
_y
= dir_y
# direction of scaling y
197 # initial origin of scaling = M(to original transform) * (ox, oy)
198 iov
= compat
.matmul(mat
, mathutils
.Vector((ox
, oy
, 0.0)))
199 self
.__iox
= iov
.x
# initial origin of scaling X
200 self
.__ioy
= iov
.y
# initial origin of scaling y
204 mat = M(to original transform)^-1 * Mt(to origin) * Ms *
205 Mt(to origin)^-1 * M(to original transform)
208 mi
= self
.__mat
.inverted()
209 mtoi
= mathutils
.Matrix
.Translation((-self
.__iox
, -self
.__ioy
, 0.0))
210 mto
= mathutils
.Matrix
.Translation((self
.__iox
, self
.__ioy
, 0.0))
211 # every point must be transformed to origin
212 t
= compat
.matmul(m
, mathutils
.Vector((self
.__ix
, self
.__iy
, 0.0)))
214 t
= compat
.matmul(m
, mathutils
.Vector((self
.__ox
, self
.__oy
, 0.0)))
216 t
= compat
.matmul(m
, mathutils
.Vector((self
.__x
, self
.__y
, 0.0)))
218 ms
= mathutils
.Matrix()
220 if self
.__dir
_x
== 1:
221 ms
[0][0] = (tx
- tox
) * self
.__dir
_x
/ (tix
- tox
)
222 if self
.__dir
_y
== 1:
223 ms
[1][1] = (ty
- toy
) * self
.__dir
_y
/ (tiy
- toy
)
224 return compat
.matmul(compat
.matmul(compat
.matmul(
225 compat
.matmul(mi
, mto
), ms
), mtoi
), m
)
232 class UniformScalingCommand(CommandBase
):
234 Custom class: Uniform Scaling operation
237 def __init__(self
, ix
, iy
, ox
, oy
, mat
):
240 self
.__ix
= ix
# initial x
241 self
.__iy
= iy
# initial y
242 self
.__x
= ix
# current x
243 self
.__y
= iy
# current y
244 self
.__ox
= ox
# origin of scaling x
245 self
.__oy
= oy
# origin of scaling y
247 # initial origin of scaling = M(to original transform) * (ox, oy)
248 iov
= compat
.matmul(mat
, mathutils
.Vector((ox
, oy
, 0.0)))
249 self
.__iox
= iov
.x
# initial origin of scaling x
250 self
.__ioy
= iov
.y
# initial origin of scaling y
256 mat = M(to original transform)^-1 * Mt(to origin) * Ms *
257 Mt(to origin)^-1 * M(to original transform)
260 mi
= self
.__mat
.inverted()
261 mtoi
= mathutils
.Matrix
.Translation((-self
.__iox
, -self
.__ioy
, 0.0))
262 mto
= mathutils
.Matrix
.Translation((self
.__iox
, self
.__ioy
, 0.0))
263 # every point must be transformed to origin
264 t
= compat
.matmul(m
, mathutils
.Vector((self
.__ix
, self
.__iy
, 0.0)))
266 t
= compat
.matmul(m
, mathutils
.Vector((self
.__ox
, self
.__oy
, 0.0)))
268 t
= compat
.matmul(m
, mathutils
.Vector((self
.__x
, self
.__y
, 0.0)))
270 ms
= mathutils
.Matrix()
272 tir
= math
.sqrt((tix
- tox
) * (tix
- tox
) + (tiy
- toy
) * (tiy
- toy
))
273 tr
= math
.sqrt((tx
- tox
) * (tx
- tox
) + (ty
- toy
) * (ty
- toy
))
277 if ((tx
- tox
) * (tix
- tox
)) > 0:
281 if ((ty
- toy
) * (tiy
- toy
)) > 0:
286 ms
[0][0] = sr
* self
.__dir
_x
287 ms
[1][1] = sr
* self
.__dir
_y
289 return compat
.matmul(compat
.matmul(compat
.matmul(
290 compat
.matmul(mi
, mto
), ms
), mtoi
), m
)
297 class CommandExecuter
:
299 Custom class: manage command history and execute command
303 self
.__cmd
_list
= [] # history
304 self
.__cmd
_list
_redo
= [] # redo list
306 def execute(self
, begin
=0, end
=-1):
308 create matrix from history
310 mat
= mathutils
.Matrix()
312 for i
, cmd
in enumerate(self
.__cmd
_list
):
313 if begin
<= i
and (end
== -1 or i
<= end
):
314 mat
= compat
.matmul(cmd
.to_matrix(), mat
)
321 return len(self
.__cmd
_list
)
327 if len(self
.__cmd
_list
) <= 0:
329 return self
.__cmd
_list
[-1]
331 def append(self
, cmd
):
335 self
.__cmd
_list
.append(cmd
)
336 self
.__cmd
_list
_redo
= []
342 if len(self
.__cmd
_list
) <= 0:
344 self
.__cmd
_list
_redo
.append(self
.__cmd
_list
.pop())
350 if len(self
.__cmd
_list
_redo
) <= 0:
352 self
.__cmd
_list
.append(self
.__cmd
_list
_redo
.pop())
355 if len(self
.__cmd
_list
) <= 0:
357 return self
.__cmd
_list
.pop()
360 self
.__cmd
_list
.append(cmd
)
363 class State(IntEnum
):
365 Enum: State definition used by MUV_UVBBStateMgr
378 UNIFORM_SCALING_1
= 11
379 UNIFORM_SCALING_2
= 12
380 UNIFORM_SCALING_3
= 13
381 UNIFORM_SCALING_4
= 14
386 Custom class: Base class of state
392 def update(self
, context
, event
, ctrl_points
, mouse_view
):
393 raise NotImplementedError
396 class StateNone(StateBase
):
400 Wait for event from mouse
403 def __init__(self
, cmd_exec
):
405 self
.__cmd
_exec
= cmd_exec
407 def update(self
, context
, event
, ctrl_points
, mouse_view
):
411 user_prefs
= compat
.get_user_preferences(context
)
412 prefs
= user_prefs
.addons
["magic_uv"].preferences
413 cp_react_size
= prefs
.uv_bounding_box_cp_react_size
414 is_uscaling
= context
.scene
.muv_uv_bounding_box_uniform_scaling
415 if (event
.type == 'LEFTMOUSE') and (event
.value
== 'PRESS'):
416 x
, y
= context
.region
.view2d
.view_to_region(
417 mouse_view
.x
, mouse_view
.y
)
418 for i
, p
in enumerate(ctrl_points
):
419 px
, py
= context
.region
.view2d
.view_to_region(p
.x
, p
.y
)
420 in_cp_x
= px
- cp_react_size
< x
< px
+ cp_react_size
421 in_cp_y
= py
- cp_react_size
< y
< py
+ cp_react_size
422 if in_cp_x
and in_cp_y
:
427 State
.UNIFORM_SCALING_1
+
431 return State
.TRANSLATING
+ i
436 class StateTranslating(StateBase
):
438 Custom class: Translating state
441 def __init__(self
, cmd_exec
, ctrl_points
):
443 self
.__cmd
_exec
= cmd_exec
444 ix
, iy
= ctrl_points
[0].x
, ctrl_points
[0].y
445 self
.__cmd
_exec
.append(TranslationCommand(ix
, iy
))
447 def update(self
, context
, event
, ctrl_points
, mouse_view
):
448 if event
.type == 'LEFTMOUSE':
449 if event
.value
== 'RELEASE':
451 if event
.type == 'MOUSEMOVE':
452 x
, y
= mouse_view
.x
, mouse_view
.y
453 self
.__cmd
_exec
.top().set(x
, y
)
454 return State
.TRANSLATING
457 class StateScaling(StateBase
):
459 Custom class: Scaling state
462 def __init__(self
, cmd_exec
, state
, ctrl_points
):
465 self
.__cmd
_exec
= cmd_exec
466 dir_x_list
= [1, 1, 1, 0, 0, 1, 1, 1]
467 dir_y_list
= [1, 0, 1, 1, 1, 1, 0, 1]
469 ix
, iy
= ctrl_points
[idx
+ 1].x
, ctrl_points
[idx
+ 1].y
470 ox
, oy
= ctrl_points
[8 - idx
].x
, ctrl_points
[8 - idx
].y
471 dir_x
, dir_y
= dir_x_list
[idx
], dir_y_list
[idx
]
472 mat
= self
.__cmd
_exec
.execute(end
=self
.__cmd
_exec
.undo_size())
473 self
.__cmd
_exec
.append(
474 ScalingCommand(ix
, iy
, ox
, oy
, dir_x
, dir_y
, mat
.inverted()))
476 def update(self
, context
, event
, ctrl_points
, mouse_view
):
477 if event
.type == 'LEFTMOUSE':
478 if event
.value
== 'RELEASE':
480 if event
.type == 'MOUSEMOVE':
481 x
, y
= mouse_view
.x
, mouse_view
.y
482 self
.__cmd
_exec
.top().set(x
, y
)
486 class StateUniformScaling(StateBase
):
488 Custom class: Uniform Scaling state
491 def __init__(self
, cmd_exec
, state
, ctrl_points
):
494 self
.__cmd
_exec
= cmd_exec
495 icp_idx
= [1, 3, 6, 8]
496 ocp_idx
= [8, 6, 3, 1]
497 idx
= state
- State
.UNIFORM_SCALING_1
498 ix
, iy
= ctrl_points
[icp_idx
[idx
]].x
, ctrl_points
[icp_idx
[idx
]].y
499 ox
, oy
= ctrl_points
[ocp_idx
[idx
]].x
, ctrl_points
[ocp_idx
[idx
]].y
500 mat
= self
.__cmd
_exec
.execute(end
=self
.__cmd
_exec
.undo_size())
501 self
.__cmd
_exec
.append(UniformScalingCommand(
502 ix
, iy
, ox
, oy
, mat
.inverted()))
504 def update(self
, context
, event
, ctrl_points
, mouse_view
):
505 if event
.type == 'LEFTMOUSE':
506 if event
.value
== 'RELEASE':
508 if event
.type == 'MOUSEMOVE':
509 x
, y
= mouse_view
.x
, mouse_view
.y
510 self
.__cmd
_exec
.top().set(x
, y
)
515 class StateRotating(StateBase
):
517 Custom class: Rotating state
520 def __init__(self
, cmd_exec
, ctrl_points
):
522 self
.__cmd
_exec
= cmd_exec
523 ix
, iy
= ctrl_points
[9].x
, ctrl_points
[9].y
524 ox
, oy
= ctrl_points
[0].x
, ctrl_points
[0].y
525 self
.__cmd
_exec
.append(RotationCommand(ix
, iy
, ox
, oy
))
527 def update(self
, context
, event
, ctrl_points
, mouse_view
):
528 if event
.type == 'LEFTMOUSE':
529 if event
.value
== 'RELEASE':
531 if event
.type == 'MOUSEMOVE':
532 x
, y
= mouse_view
.x
, mouse_view
.y
533 self
.__cmd
_exec
.top().set(x
, y
)
534 return State
.ROTATING
539 Custom class: Manage state about this feature
542 def __init__(self
, cmd_exec
):
543 self
.__cmd
_exec
= cmd_exec
# command executer
544 self
.__state
= State
.NONE
# current state
545 self
.__state
_obj
= StateNone(self
.__cmd
_exec
)
547 def __update_state(self
, next_state
, ctrl_points
):
552 if next_state
== self
.__state
:
555 if next_state
== State
.TRANSLATING
:
556 obj
= StateTranslating(self
.__cmd
_exec
, ctrl_points
)
557 elif State
.SCALING_1
<= next_state
<= State
.SCALING_8
:
559 self
.__cmd
_exec
, next_state
, ctrl_points
)
560 elif next_state
== State
.ROTATING
:
561 obj
= StateRotating(self
.__cmd
_exec
, ctrl_points
)
562 elif next_state
== State
.NONE
:
563 obj
= StateNone(self
.__cmd
_exec
)
564 elif (State
.UNIFORM_SCALING_1
<= next_state
<=
565 State
.UNIFORM_SCALING_4
):
566 obj
= StateUniformScaling(
567 self
.__cmd
_exec
, next_state
, ctrl_points
)
570 self
.__state
_obj
= obj
572 self
.__state
= next_state
574 def update(self
, context
, ctrl_points
, event
):
575 mouse_region
= mathutils
.Vector((
576 event
.mouse_region_x
, event
.mouse_region_y
))
577 mouse_view
= mathutils
.Vector((context
.region
.view2d
.region_to_view(
578 mouse_region
.x
, mouse_region
.y
)))
579 next_state
= self
.__state
_obj
.update(
580 context
, event
, ctrl_points
, mouse_view
)
581 self
.__update
_state
(next_state
, ctrl_points
)
587 class MUV_OT_UVBoundingBox(bpy
.types
.Operator
):
589 Operation class: UV Bounding Box
592 bl_idname
= "uv.muv_uv_bounding_box"
593 bl_label
= "UV Bounding Box"
594 bl_description
= "Internal operation for UV Bounding Box"
595 bl_options
= {'REGISTER', 'UNDO'}
599 self
.__cmd
_exec
= CommandExecuter() # Command executor
600 self
.__state
_mgr
= StateManager(self
.__cmd
_exec
) # State Manager
606 def poll(cls
, context
):
607 # we can not get area/space/region from console
608 if common
.is_console_mode():
610 return _is_valid_context(context
)
613 def is_running(cls
, _
):
614 return 1 if cls
.__handle
else 0
617 def handle_add(cls
, obj
, context
):
618 if cls
.__handle
is None:
619 sie
= bpy
.types
.SpaceImageEditor
620 cls
.__handle
= sie
.draw_handler_add(
621 cls
.draw_bb
, (obj
, context
), "WINDOW", "POST_PIXEL")
622 if cls
.__timer
is None:
623 cls
.__timer
= context
.window_manager
.event_timer_add(
624 0.1, window
=context
.window
)
625 context
.window_manager
.modal_handler_add(obj
)
628 def handle_remove(cls
, context
):
629 if cls
.__handle
is not None:
630 sie
= bpy
.types
.SpaceImageEditor
631 sie
.draw_handler_remove(cls
.__handle
, "WINDOW")
633 if cls
.__timer
is not None:
634 context
.window_manager
.event_timer_remove(cls
.__timer
)
638 def draw_bb(cls
, _
, context
):
642 props
= context
.scene
.muv_props
.uv_bounding_box
644 if not MUV_OT_UVBoundingBox
.is_running(context
):
647 if not _is_valid_context(context
):
650 user_prefs
= compat
.get_user_preferences(context
)
651 prefs
= user_prefs
.addons
["magic_uv"].preferences
652 cp_size
= prefs
.uv_bounding_box_cp_size
654 gpu
.state
.program_point_size_set(False)
655 gpu
.state
.point_size_set(cp_size
)
656 gpu
.state
.blend_set('ALPHA')
658 shader
= gpu
.shader
.from_builtin("UNIFORM_COLOR")
660 shader
.uniform_float("color", (1.0, 1.0, 1.0, 1.0))
662 points
= [mathutils
.Vector(context
.region
.view2d
.view_to_region(cp
.x
, cp
.y
)) for cp
in props
.ctrl_points
]
663 batch
= batch_for_shader(shader
, 'POINTS', {"pos": points
})
667 def __get_uv_info(self
, context
):
672 objs
= common
.get_uv_editable_objects(context
)
676 bm
= bmesh
.from_edit_mesh(obj
.data
)
677 if common
.check_version(2, 73, 0) >= 0:
678 bm
.faces
.ensure_lookup_table()
679 if not bm
.loops
.layers
.uv
:
681 uv_layer
= bm
.loops
.layers
.uv
.verify()
685 for i
, l
in enumerate(f
.loops
):
686 if sc
.muv_uv_bounding_box_boundary
== 'UV_SEL':
687 if l
[uv_layer
].select
:
692 "uv": l
[uv_layer
].uv
.copy()
694 elif sc
.muv_uv_bounding_box_boundary
== 'UV':
699 "uv": l
[uv_layer
].uv
.copy()
705 def __get_ctrl_point(self
, uv_info_ini
):
714 for info
in uv_info_ini
:
727 (left
+ right
) * 0.5, (top
+ bottom
) * 0.5, 0.0
729 mathutils
.Vector((left
, top
, 0.0)),
730 mathutils
.Vector((left
, (top
+ bottom
) * 0.5, 0.0)),
731 mathutils
.Vector((left
, bottom
, 0.0)),
732 mathutils
.Vector(((left
+ right
) * 0.5, top
, 0.0)),
733 mathutils
.Vector(((left
+ right
) * 0.5, bottom
, 0.0)),
734 mathutils
.Vector((right
, top
, 0.0)),
735 mathutils
.Vector((right
, (top
+ bottom
) * 0.5, 0.0)),
736 mathutils
.Vector((right
, bottom
, 0.0)),
737 mathutils
.Vector(((left
+ right
) * 0.5, top
+ 0.03, 0.0))
742 def __update_uvs(self
, context
, uv_info_ini
, trans_mat
):
747 for info
in uv_info_ini
:
749 uv_layer
= bm
.loops
.layers
.uv
.verify()
753 v
= mathutils
.Vector((uv
.x
, uv
.y
, 0.0))
754 av
= compat
.matmul(trans_mat
, v
)
755 bm
.faces
[fidx
].loops
[lidx
][uv_layer
].uv
= mathutils
.Vector(
758 objs
= common
.get_uv_editable_objects(context
)
760 bmesh
.update_edit_mesh(obj
.data
)
762 def __update_ctrl_point(self
, ctrl_points_ini
, trans_mat
):
766 return [compat
.matmul(trans_mat
, cp
) for cp
in ctrl_points_ini
]
768 def modal(self
, context
, event
):
769 props
= context
.scene
.muv_props
.uv_bounding_box
770 common
.redraw_all_areas()
772 if not MUV_OT_UVBoundingBox
.is_running(context
):
775 if not _is_valid_context(context
):
776 MUV_OT_UVBoundingBox
.handle_remove(context
)
784 if not common
.mouse_on_area(event
, 'IMAGE_EDITOR') or \
785 common
.mouse_on_regions(event
, 'IMAGE_EDITOR', region_types
):
786 return {'PASS_THROUGH'}
788 if event
.type == 'TIMER':
789 trans_mat
= self
.__cmd
_exec
.execute()
790 self
.__update
_uvs
(context
, props
.uv_info_ini
, trans_mat
)
791 props
.ctrl_points
= self
.__update
_ctrl
_point
(
792 props
.ctrl_points_ini
, trans_mat
)
794 state
= self
.__state
_mgr
.update(context
, props
.ctrl_points
, event
)
795 if state
== State
.NONE
:
796 return {'PASS_THROUGH'}
798 return {'RUNNING_MODAL'}
800 def invoke(self
, context
, _
):
801 props
= context
.scene
.muv_props
.uv_bounding_box
803 if MUV_OT_UVBoundingBox
.is_running(context
):
804 MUV_OT_UVBoundingBox
.handle_remove(context
)
807 props
.uv_info_ini
= self
.__get
_uv
_info
(context
)
808 if props
.uv_info_ini
is None:
811 MUV_OT_UVBoundingBox
.handle_add(self
, context
)
813 props
.ctrl_points_ini
= self
.__get
_ctrl
_point
(props
.uv_info_ini
)
814 trans_mat
= self
.__cmd
_exec
.execute()
815 # Update is needed in order to display control point
816 self
.__update
_uvs
(context
, props
.uv_info_ini
, trans_mat
)
817 props
.ctrl_points
= self
.__update
_ctrl
_point
(
818 props
.ctrl_points_ini
, trans_mat
)
820 return {'RUNNING_MODAL'}