uv_magic_uv commit to addons release: T51064

Initial commit to addons release.
Task: T51064
This commit is contained in:
nutti 2017-04-02 11:21:28 +09:00
parent d9c25b4390
commit 8a17c01627
18 changed files with 4238 additions and 0 deletions

126
uv_magic_uv/__init__.py Normal file
View File

@ -0,0 +1,126 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
bl_info = {
"name": "Magic UV",
"author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, Keith (Wahooney) Boshoff, McBuff, MaxRobinot",
"version": (4, 3),
"blender": (2, 77, 0),
"location": "See Add-ons Preferences",
"description": "UV Manipulator Tools. See Add-ons Preferences for details",
"warning": "",
"support": "COMMUNITY",
"wiki_url": "https://github.com/nutti/Magic-UV/wikil",
"tracker_url": "https://github.com/nutti/Copy-And-Paste-UV",
"category": "UV"
}
if "bpy" in locals():
import imp
imp.reload(muv_preferences)
imp.reload(muv_menu)
imp.reload(muv_common)
imp.reload(muv_props)
imp.reload(muv_cpuv_ops)
imp.reload(muv_cpuv_selseq_ops)
imp.reload(muv_fliprot_ops)
imp.reload(muv_transuv_ops)
imp.reload(muv_uvbb_ops)
imp.reload(muv_mvuv_ops)
imp.reload(muv_texproj_ops)
imp.reload(muv_packuv_ops)
imp.reload(muv_texlock_ops)
imp.reload(muv_mirroruv_ops)
imp.reload(muv_wsuv_ops)
imp.reload(muv_unwrapconst_ops)
imp.reload(muv_preserve_uv_aspect)
else:
from . import muv_preferences
from . import muv_menu
from . import muv_common
from . import muv_props
from . import muv_cpuv_ops
from . import muv_cpuv_selseq_ops
from . import muv_fliprot_ops
from . import muv_transuv_ops
from . import muv_uvbb_ops
from . import muv_mvuv_ops
from . import muv_texproj_ops
from . import muv_packuv_ops
from . import muv_texlock_ops
from . import muv_mirroruv_ops
from . import muv_wsuv_ops
from . import muv_unwrapconst_ops
from . import muv_preserve_uv_aspect
import bpy
def view3d_uvmap_menu_fn(self, context):
self.layout.separator()
self.layout.menu(muv_menu.MUV_CPUVMenu.bl_idname, icon="PLUGIN")
self.layout.operator(muv_fliprot_ops.MUV_FlipRot.bl_idname, icon="PLUGIN")
self.layout.menu(muv_menu.MUV_TransUVMenu.bl_idname, icon="PLUGIN")
self.layout.operator(muv_mvuv_ops.MUV_MVUV.bl_idname, icon="PLUGIN")
self.layout.menu(muv_menu.MUV_TexLockMenu.bl_idname, icon="PLUGIN")
self.layout.operator(
muv_mirroruv_ops.MUV_MirrorUV.bl_idname, icon="PLUGIN")
self.layout.menu(muv_menu.MUV_WSUVMenu.bl_idname, icon="PLUGIN")
self.layout.operator(
muv_unwrapconst_ops.MUV_UnwrapConstraint.bl_idname, icon='PLUGIN')
self.layout.menu(
muv_preserve_uv_aspect.MUV_PreserveUVAspectMenu.bl_idname,
icon='PLUGIN')
def image_uvs_menu_fn(self, context):
self.layout.separator()
self.layout.operator(muv_packuv_ops.MUV_PackUV.bl_idname, icon="PLUGIN")
def view3d_object_menu_fn(self, context):
self.layout.separator()
self.layout.menu(muv_menu.MUV_CPUVObjMenu.bl_idname, icon="PLUGIN")
def register():
bpy.utils.register_module(__name__)
bpy.types.VIEW3D_MT_uv_map.append(view3d_uvmap_menu_fn)
bpy.types.IMAGE_MT_uvs.append(image_uvs_menu_fn)
bpy.types.VIEW3D_MT_object.append(view3d_object_menu_fn)
muv_props.init_props(bpy.types.Scene)
def unregister():
bpy.utils.unregister_module(__name__)
bpy.types.VIEW3D_MT_uv_map.remove(view3d_uvmap_menu_fn)
bpy.types.IMAGE_MT_uvs.remove(image_uvs_menu_fn)
bpy.types.VIEW3D_MT_object.remove(view3d_object_menu_fn)
muv_props.clear_props(bpy.types.Scene)
if __name__ == "__main__":
register()

86
uv_magic_uv/muv_common.py Normal file
View File

@ -0,0 +1,86 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
from . import muv_props
def debug_print(*s):
"""
Print message to console in debugging mode
"""
if muv_props.DEBUG:
print(s)
def check_version(major, minor, _):
"""
Check blender version
"""
if bpy.app.version[0] == major and bpy.app.version[1] == minor:
return 0
if bpy.app.version[0] > major:
return 1
else:
if bpy.app.version[1] > minor:
return 1
else:
return -1
def redraw_all_areas():
"""
Redraw all areas
"""
for area in bpy.context.screen.areas:
area.tag_redraw()
def get_space(area_type, region_type, space_type):
"""
Get current area/region/space
"""
area = None
region = None
space = None
for area in bpy.context.screen.areas:
if area.type == area_type:
break
else:
return (None, None, None)
for region in area.regions:
if region.type == region_type:
break
for space in area.spaces:
if space.type == space_type:
break
return (area, region, space)

455
uv_magic_uv/muv_cpuv_ops.py Normal file
View File

@ -0,0 +1,455 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
import bmesh
from bpy.props import StringProperty, BoolProperty, IntProperty, EnumProperty
from . import muv_common
def memorize_view_3d_mode(fn):
def __memorize_view_3d_mode(self, context):
mode_orig = bpy.context.object.mode
result = fn(self, context)
bpy.ops.object.mode_set(mode=mode_orig)
return result
return __memorize_view_3d_mode
class MUV_CPUVCopyUV(bpy.types.Operator):
"""
Operation class: Copy UV coordinate
"""
bl_idname = "uv.muv_cpuv_copy_uv"
bl_label = "Copy UV (Operation)"
bl_description = "Copy UV coordinate (Operation)"
bl_options = {'REGISTER', 'UNDO'}
uv_map = StringProperty(options={'HIDDEN'})
def execute(self, context):
props = context.scene.muv_props.cpuv
if self.uv_map == "":
self.report({'INFO'}, "Copy UV coordinate")
else:
self.report(
{'INFO'}, "Copy UV coordinate (UV map:%s)" % (self.uv_map))
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
# get UV layer
if self.uv_map == "":
if not bm.loops.layers.uv:
self.report(
{'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
else:
uv_layer = bm.loops.layers.uv[self.uv_map]
# get selected face
props.src_uvs = []
props.src_pin_uvs = []
for face in bm.faces:
if face.select:
uvs = [l[uv_layer].uv.copy() for l in face.loops]
pin_uvs = [l[uv_layer].pin_uv for l in face.loops]
props.src_uvs.append(uvs)
props.src_pin_uvs.append(pin_uvs)
if len(props.src_uvs) == 0 or len(props.src_pin_uvs) == 0:
self.report({'WARNING'}, "No faces are selected")
return {'CANCELLED'}
self.report({'INFO'}, "%d face(s) are selected" % len(props.src_uvs))
return {'FINISHED'}
class MUV_CPUVCopyUVMenu(bpy.types.Menu):
"""
Menu class: Copy UV coordinate
"""
bl_idname = "uv.muv_cpuv_copy_uv_menu"
bl_label = "Copy UV"
bl_description = "Copy UV coordinate"
def draw(self, context):
layout = self.layout
# create sub menu
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
uv_maps = bm.loops.layers.uv.keys()
layout.operator(
MUV_CPUVCopyUV.bl_idname,
text="[Default]",
icon="PLUGIN"
).uv_map = ""
for m in uv_maps:
layout.operator(
MUV_CPUVCopyUV.bl_idname,
text=m,
icon="PLUGIN"
).uv_map = m
class MUV_CPUVPasteUV(bpy.types.Operator):
"""
Operation class: Paste UV coordinate
"""
bl_idname = "uv.muv_cpuv_paste_uv"
bl_label = "Paste UV (Operation)"
bl_description = "Paste UV coordinate (Operation)"
bl_options = {'REGISTER', 'UNDO'}
uv_map = StringProperty(options={'HIDDEN'})
strategy = EnumProperty(
name="Strategy",
description="Paste Strategy",
items=[
('N_N', 'N:N', 'Number of faces must be equal to source'),
('N_M', 'N:M', 'Number of faces must not be equal to source')
],
default="N_M"
)
flip_copied_uv = BoolProperty(
name="Flip Copied UV",
description="Flip Copied UV...",
default=False
)
rotate_copied_uv = IntProperty(
default=0,
name="Rotate Copied UV",
min=0,
max=30
)
def execute(self, context):
props = context.scene.muv_props.cpuv
if len(props.src_uvs) == 0 or len(props.src_pin_uvs) == 0:
self.report({'WARNING'}, "Need copy UV at first")
return {'CANCELLED'}
if self.uv_map == "":
self.report({'INFO'}, "Paste UV coordinate")
else:
self.report(
{'INFO'}, "Paste UV coordinate (UV map:%s)" % (self.uv_map))
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
# get UV layer
if self.uv_map == "":
if not bm.loops.layers.uv:
self.report(
{'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
else:
uv_layer = bm.loops.layers.uv[self.uv_map]
# get selected face
dest_uvs = []
dest_pin_uvs = []
dest_face_indices = []
for face in bm.faces:
if face.select:
dest_face_indices.append(face.index)
uvs = [l[uv_layer].uv.copy() for l in face.loops]
pin_uvs = [l[uv_layer].pin_uv for l in face.loops]
dest_uvs.append(uvs)
dest_pin_uvs.append(pin_uvs)
if len(dest_uvs) == 0 or len(dest_pin_uvs) == 0:
self.report({'WARNING'}, "No faces are selected")
return {'CANCELLED'}
if self.strategy == 'N_N' and len(props.src_uvs) != len(dest_uvs):
self.report(
{'WARNING'},
"Number of selected faces is different from copied" +
"(src:%d, dest:%d)" %
(len(props.src_uvs), len(dest_uvs)))
return {'CANCELLED'}
# paste
for i, idx in enumerate(dest_face_indices):
suv = None
spuv = None
duv = None
if self.strategy == 'N_N':
suv = props.src_uvs[i]
spuv = props.src_pin_uvs[i]
duv = dest_uvs[i]
elif self.strategy == 'N_M':
suv = props.src_uvs[i % len(props.src_uvs)]
spuv = props.src_pin_uvs[i % len(props.src_pin_uvs)]
duv = dest_uvs[i]
if len(suv) != len(duv):
self.report({'WARNING'}, "Some faces are different size")
return {'CANCELLED'}
suvs_fr = [uv for uv in suv]
spuvs_fr = [pin_uv for pin_uv in spuv]
# flip UVs
if self.flip_copied_uv is True:
suvs_fr.reverse()
spuvs_fr.reverse()
# rotate UVs
for _ in range(self.rotate_copied_uv):
uv = suvs_fr.pop()
pin_uv = spuvs_fr.pop()
suvs_fr.insert(0, uv)
spuvs_fr.insert(0, pin_uv)
# paste UVs
for l, suv, spuv in zip(bm.faces[idx].loops, suvs_fr, spuvs_fr):
l[uv_layer].uv = suv
l[uv_layer].pin_uv = spuv
self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs))
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}
class MUV_CPUVPasteUVMenu(bpy.types.Menu):
"""
Menu class: Paste UV coordinate
"""
bl_idname = "uv.muv_cpuv_paste_uv_menu"
bl_label = "Paste UV"
bl_description = "Paste UV coordinate"
def draw(self, context):
layout = self.layout
# create sub menu
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
uv_maps = bm.loops.layers.uv.keys()
layout.operator(
MUV_CPUVPasteUV.bl_idname,
text="[Default]", icon="PLUGIN").uv_map = ""
for m in uv_maps:
layout.operator(
MUV_CPUVPasteUV.bl_idname,
text=m, icon="PLUGIN").uv_map = m
class MUV_CPUVObjCopyUV(bpy.types.Operator):
"""
Operation class: Copy UV coordinate per object
"""
bl_idname = "object.muv_cpuv_obj_copy_uv"
bl_label = "Copy UV"
bl_description = "Copy UV coordinate"
bl_options = {'REGISTER', 'UNDO'}
uv_map = StringProperty(options={'HIDDEN'})
@memorize_view_3d_mode
def execute(self, context):
props = context.scene.muv_props.cpuv_obj
if self.uv_map == "":
self.report({'INFO'}, "Copy UV coordinate per object")
else:
self.report(
{'INFO'},
"Copy UV coordinate per object (UV map:%s)" % (self.uv_map))
bpy.ops.object.mode_set(mode='EDIT')
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
# get UV layer
if self.uv_map == "":
if not bm.loops.layers.uv:
self.report(
{'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
else:
uv_layer = bm.loops.layers.uv[self.uv_map]
# get selected face
props.src_uvs = []
props.src_pin_uvs = []
for face in bm.faces:
uvs = [l[uv_layer].uv.copy() for l in face.loops]
pin_uvs = [l[uv_layer].pin_uv for l in face.loops]
props.src_uvs.append(uvs)
props.src_pin_uvs.append(pin_uvs)
self.report({'INFO'}, "%s's UV coordinates are copied" % (obj.name))
return {'FINISHED'}
class MUV_CPUVObjCopyUVMenu(bpy.types.Menu):
"""
Menu class: Copy UV coordinate per object
"""
bl_idname = "object.muv_cpuv_obj_copy_uv_menu"
bl_label = "Copy UV"
bl_description = "Copy UV coordinate per object"
def draw(self, _):
layout = self.layout
# create sub menu
uv_maps = bpy.context.active_object.data.uv_textures.keys()
layout.operator(
MUV_CPUVObjCopyUV.bl_idname,
text="[Default]", icon="PLUGIN").uv_map = ""
for m in uv_maps:
layout.operator(
MUV_CPUVObjCopyUV.bl_idname,
text=m, icon="PLUGIN").uv_map = m
class MUV_CPUVObjPasteUV(bpy.types.Operator):
"""
Operation class: Paste UV coordinate per object
"""
bl_idname = "object.muv_cpuv_obj_paste_uv"
bl_label = "Paste UV"
bl_description = "Paste UV coordinate"
bl_options = {'REGISTER', 'UNDO'}
uv_map = StringProperty(options={'HIDDEN'})
@memorize_view_3d_mode
def execute(self, context):
props = context.scene.muv_props.cpuv_obj
if len(props.src_uvs) == 0 or len(props.src_pin_uvs) == 0:
self.report({'WARNING'}, "Need copy UV at first")
return {'CANCELLED'}
for o in bpy.data.objects:
if not hasattr(o.data, "uv_textures") or not o.select:
continue
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.scene.objects.active = o
bpy.ops.object.mode_set(mode='EDIT')
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
if (self.uv_map == "" or
self.uv_map not in bm.loops.layers.uv.keys()):
self.report({'INFO'}, "Paste UV coordinate per object")
else:
self.report(
{'INFO'},
"Paste UV coordinate per object (UV map: %s)"
% (self.uv_map))
# get UV layer
if (self.uv_map == "" or
self.uv_map not in bm.loops.layers.uv.keys()):
if not bm.loops.layers.uv:
self.report(
{'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
else:
uv_layer = bm.loops.layers.uv[self.uv_map]
# get selected face
dest_uvs = []
dest_pin_uvs = []
dest_face_indices = []
for face in bm.faces:
dest_face_indices.append(face.index)
uvs = [l[uv_layer].uv.copy() for l in face.loops]
pin_uvs = [l[uv_layer].pin_uv for l in face.loops]
dest_uvs.append(uvs)
dest_pin_uvs.append(pin_uvs)
if len(props.src_uvs) != len(dest_uvs):
self.report(
{'WARNING'},
"Number of faces is different from copied "
+ "(src:%d, dest:%d)"
% (len(props.src_uvs), len(dest_uvs))
)
return {'CANCELLED'}
# paste
for i, idx in enumerate(dest_face_indices):
suv = props.src_uvs[i]
spuv = props.src_pin_uvs[i]
duv = dest_uvs[i]
if len(suv) != len(duv):
self.report({'WARNING'}, "Some faces are different size")
return {'CANCELLED'}
suvs_fr = [uv for uv in suv]
spuvs_fr = [pin_uv for pin_uv in spuv]
# paste UVs
for l, suv, spuv in zip(
bm.faces[idx].loops, suvs_fr, spuvs_fr):
l[uv_layer].uv = suv
l[uv_layer].pin_uv = spuv
bmesh.update_edit_mesh(obj.data)
self.report(
{'INFO'}, "%s's UV coordinates are pasted" % (obj.name))
return {'FINISHED'}
class MUV_CPUVObjPasteUVMenu(bpy.types.Menu):
"""
Menu class: Paste UV coordinate per object
"""
bl_idname = "object.muv_cpuv_obj_paste_uv_menu"
bl_label = "Paste UV"
bl_description = "Paste UV coordinate per object"
def draw(self, _):
layout = self.layout
# create sub menu
uv_maps = []
for obj in bpy.data.objects:
if hasattr(obj.data, "uv_textures") and obj.select:
uv_maps.extend(obj.data.uv_textures.keys())
uv_maps = list(set(uv_maps))
layout.operator(
MUV_CPUVObjPasteUV.bl_idname,
text="[Default]", icon="PLUGIN").uv_map = ""
for m in uv_maps:
layout.operator(
MUV_CPUVObjPasteUV.bl_idname,
text=m, icon="PLUGIN").uv_map = m

View File

@ -0,0 +1,249 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
import bmesh
from bpy.props import StringProperty, BoolProperty, IntProperty, EnumProperty
from . import muv_common
class MUV_CPUVSelSeqCopyUV(bpy.types.Operator):
"""
Operation class: Copy UV coordinate by selection sequence
"""
bl_idname = "uv.muv_cpuv_selseq_copy_uv"
bl_label = "Copy UV (Selection Sequence) (Operation)"
bl_description = "Copy UV data by selection sequence (Operation)"
bl_options = {'REGISTER', 'UNDO'}
uv_map = StringProperty(options={'HIDDEN'})
def execute(self, context):
props = context.scene.muv_props.cpuv_selseq
if self.uv_map == "":
self.report({'INFO'}, "Copy UV coordinate (selection sequence)")
else:
self.report(
{'INFO'},
"Copy UV coordinate (selection sequence) (UV map:%s)"
% (self.uv_map))
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
# get UV layer
if self.uv_map == "":
if not bm.loops.layers.uv:
self.report(
{'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
else:
uv_layer = bm.loops.layers.uv[self.uv_map]
# get selected face
props.src_uvs = []
props.src_pin_uvs = []
for hist in bm.select_history:
if isinstance(hist, bmesh.types.BMFace) and hist.select:
uvs = [l[uv_layer].uv.copy() for l in hist.loops]
pin_uvs = [l[uv_layer].pin_uv for l in hist.loops]
props.src_uvs.append(uvs)
props.src_pin_uvs.append(pin_uvs)
if len(props.src_uvs) == 0 or len(props.src_pin_uvs) == 0:
self.report({'WARNING'}, "No faces are selected")
return {'CANCELLED'}
self.report({'INFO'}, "%d face(s) are selected" % len(props.src_uvs))
return {'FINISHED'}
class MUV_CPUVSelSeqCopyUVMenu(bpy.types.Menu):
"""
Menu class: Copy UV coordinate by selection sequence
"""
bl_idname = "uv.muv_cpuv_selseq_copy_uv_menu"
bl_label = "Copy UV (Selection Sequence)"
bl_description = "Copy UV coordinate by selection sequence"
def draw(self, context):
layout = self.layout
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
uv_maps = bm.loops.layers.uv.keys()
layout.operator(
MUV_CPUVSelSeqCopyUV.bl_idname,
text="[Default]", icon="PLUGIN").uv_map = ""
for m in uv_maps:
layout.operator(
MUV_CPUVSelSeqCopyUV.bl_idname,
text=m, icon="PLUGIN").uv_map = m
class MUV_CPUVSelSeqPasteUV(bpy.types.Operator):
"""
Operation class: Paste UV coordinate by selection sequence
"""
bl_idname = "uv.muv_cpuv_selseq_paste_uv"
bl_label = "Paste UV (Selection Sequence) (Operation)"
bl_description = "Paste UV coordinate by selection sequence (Operation)"
bl_options = {'REGISTER', 'UNDO'}
uv_map = StringProperty(options={'HIDDEN'})
strategy = EnumProperty(
name="Strategy",
description="Paste Strategy",
items=[
('N_N', 'N:N', 'Number of faces must be equal to source'),
('N_M', 'N:M', 'Number of faces must not be equal to source')
],
default="N_M")
flip_copied_uv = BoolProperty(
name="Flip Copied UV",
description="Flip Copied UV...",
default=False)
rotate_copied_uv = IntProperty(
default=0,
name="Rotate Copied UV",
min=0,
max=30)
def execute(self, context):
props = context.scene.muv_props.cpuv_selseq
if len(props.src_uvs) == 0 or len(props.src_pin_uvs) == 0:
self.report({'WARNING'}, "Need copy UV at first")
return {'CANCELLED'}
if self.uv_map == "":
self.report({'INFO'}, "Paste UV coordinate (selection sequence)")
else:
self.report(
{'INFO'},
"Paste UV coordinate (selection sequence) (UV map:%s)"
% (self.uv_map))
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
# get UV layer
if self.uv_map == "":
if not bm.loops.layers.uv:
self.report(
{'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
else:
uv_layer = bm.loops.layers.uv[self.uv_map]
# get selected face
dest_uvs = []
dest_pin_uvs = []
dest_face_indices = []
for hist in bm.select_history:
if isinstance(hist, bmesh.types.BMFace) and hist.select:
dest_face_indices.append(hist.index)
uvs = [l[uv_layer].uv.copy() for l in hist.loops]
pin_uvs = [l[uv_layer].pin_uv for l in hist.loops]
dest_uvs.append(uvs)
dest_pin_uvs.append(pin_uvs)
if len(dest_uvs) == 0 or len(dest_pin_uvs) == 0:
self.report({'WARNING'}, "No faces are selected")
return {'CANCELLED'}
if self.strategy == 'N_N' and len(props.src_uvs) != len(dest_uvs):
self.report(
{'WARNING'},
"Number of selected faces is different from copied faces "
+ "(src:%d, dest:%d)"
% (len(props.src_uvs), len(dest_uvs)))
return {'CANCELLED'}
# paste
for i, idx in enumerate(dest_face_indices):
suv = None
spuv = None
duv = None
if self.strategy == 'N_N':
suv = props.src_uvs[i]
spuv = props.src_pin_uvs[i]
duv = dest_uvs[i]
elif self.strategy == 'N_M':
suv = props.src_uvs[i % len(props.src_uvs)]
spuv = props.src_pin_uvs[i % len(props.src_pin_uvs)]
duv = dest_uvs[i]
if len(suv) != len(duv):
self.report({'WARNING'}, "Some faces are different size")
return {'CANCELLED'}
suvs_fr = [uv for uv in suv]
spuvs_fr = [pin_uv for pin_uv in spuv]
# flip UVs
if self.flip_copied_uv is True:
suvs_fr.reverse()
spuvs_fr.reverse()
# rotate UVs
for _ in range(self.rotate_copied_uv):
uv = suvs_fr.pop()
pin_uv = spuvs_fr.pop()
suvs_fr.insert(0, uv)
spuvs_fr.insert(0, pin_uv)
# paste UVs
for l, suv, spuv in zip(bm.faces[idx].loops, suvs_fr, spuvs_fr):
l[uv_layer].uv = suv
l[uv_layer].pin_uv = spuv
self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs))
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}
class MUV_CPUVSelSeqPasteUVMenu(bpy.types.Menu):
"""
Menu class: Paste UV coordinate by selection sequence
"""
bl_idname = "uv.muv_cpuv_selseq_paste_uv_menu"
bl_label = "Paste UV (Selection Sequence)"
bl_description = "Paste UV coordinate by selection sequence"
def draw(self, context):
layout = self.layout
# create sub menu
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
uv_maps = bm.loops.layers.uv.keys()
layout.operator(
MUV_CPUVSelSeqPasteUV.bl_idname,
text="[Default]", icon="PLUGIN").uv_map = ""
for m in uv_maps:
layout.operator(
MUV_CPUVSelSeqPasteUV.bl_idname,
text=m, icon="PLUGIN").uv_map = m

View File

@ -0,0 +1,107 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
import bmesh
from bpy.props import BoolProperty, IntProperty
from . import muv_common
class MUV_FlipRot(bpy.types.Operator):
"""
Operation class: Flip and Rotate UV coordinate
"""
bl_idname = "uv.muv_fliprot"
bl_label = "Flip/Rotate UV"
bl_description = "Flip/Rotate UV coordinate"
bl_options = {'REGISTER', 'UNDO'}
flip = BoolProperty(
name="Flip UV",
description="Flip UV...",
default=False
)
rotate = IntProperty(
default=0,
name="Rotate UV",
min=0,
max=30
)
def execute(self, context):
self.report({'INFO'}, "Flip/Rotate UV")
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
# get UV layer
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
# get selected face
dest_uvs = []
dest_pin_uvs = []
dest_face_indices = []
for face in bm.faces:
if face.select:
dest_face_indices.append(face.index)
uvs = [l[uv_layer].uv.copy() for l in face.loops]
pin_uvs = [l[uv_layer].pin_uv for l in face.loops]
dest_uvs.append(uvs)
dest_pin_uvs.append(pin_uvs)
if len(dest_uvs) == 0 or len(dest_pin_uvs) == 0:
self.report({'WARNING'}, "No faces are selected")
return {'CANCELLED'}
self.report({'INFO'}, "%d face(s) are selected" % len(dest_uvs))
# paste
for idx, duvs, dpuvs in zip(dest_face_indices, dest_uvs, dest_pin_uvs):
duvs_fr = [uv for uv in duvs]
dpuvs_fr = [pin_uv for pin_uv in dpuvs]
# flip UVs
if self.flip is True:
duvs_fr.reverse()
dpuvs_fr.reverse()
# rotate UVs
for _ in range(self.rotate):
uv = duvs_fr.pop()
pin_uv = dpuvs_fr.pop()
duvs_fr.insert(0, uv)
dpuvs_fr.insert(0, pin_uv)
# paste UVs
for l, duv, dpuv in zip(bm.faces[idx].loops, duvs_fr, dpuvs_fr):
l[uv_layer].uv = duv
l[uv_layer].pin_uv = dpuv
self.report({'INFO'}, "%d face(s) are flipped/rotated" % len(dest_uvs))
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}

122
uv_magic_uv/muv_menu.py Normal file
View File

@ -0,0 +1,122 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
from . import muv_cpuv_ops
from . import muv_cpuv_selseq_ops
from . import muv_transuv_ops
from . import muv_texlock_ops
from . import muv_wsuv_ops
class MUV_CPUVMenu(bpy.types.Menu):
"""
Menu class: Master menu of Copy/Paste UV coordinate
"""
bl_idname = "uv.muv_cpuv_menu"
bl_label = "Copy/Paste UV"
bl_description = "Copy and Paste UV coordinate"
def draw(self, _):
self.layout.menu(
muv_cpuv_ops.MUV_CPUVCopyUVMenu.bl_idname, icon="PLUGIN")
self.layout.menu(
muv_cpuv_ops.MUV_CPUVPasteUVMenu.bl_idname, icon="PLUGIN")
self.layout.menu(
muv_cpuv_selseq_ops.MUV_CPUVSelSeqCopyUVMenu.bl_idname,
icon="PLUGIN")
self.layout.menu(
muv_cpuv_selseq_ops.MUV_CPUVSelSeqPasteUVMenu.bl_idname,
icon="PLUGIN")
class MUV_CPUVObjMenu(bpy.types.Menu):
"""
Menu class: Master menu of Copy/Paste UV coordinate per object
"""
bl_idname = "object.muv_cpuv_obj_menu"
bl_label = "Copy/Paste UV"
bl_description = "Copy and Paste UV coordinate per object"
def draw(self, _):
self.layout.menu(
muv_cpuv_ops.MUV_CPUVObjCopyUVMenu.bl_idname, icon="PLUGIN")
self.layout.menu(
muv_cpuv_ops.MUV_CPUVObjPasteUVMenu.bl_idname, icon="PLUGIN")
class MUV_TransUVMenu(bpy.types.Menu):
"""
Menu class: Master menu of Transfer UV coordinate
"""
bl_idname = "uv.muv_transuv_menu"
bl_label = "Transfer UV"
bl_description = "Transfer UV coordinate"
def draw(self, _):
self.layout.operator(
muv_transuv_ops.MUV_TransUVCopy.bl_idname, icon="PLUGIN")
self.layout.operator(
muv_transuv_ops.MUV_TransUVPaste.bl_idname, icon="PLUGIN")
class MUV_TexLockMenu(bpy.types.Menu):
"""
Menu class: Master menu of Texture Lock
"""
bl_idname = "uv.muv_texlock_menu"
bl_label = "Texture Lock"
bl_description = "Lock texture when vertices of mesh (Preserve UV)"
def draw(self, _):
self.layout.operator(
muv_texlock_ops.MUV_TexLockStart.bl_idname, icon="PLUGIN")
self.layout.operator(
muv_texlock_ops.MUV_TexLockStop.bl_idname, icon="PLUGIN")
self.layout.operator(
muv_texlock_ops.MUV_TexLockIntrStart.bl_idname, icon="PLUGIN")
self.layout.operator(
muv_texlock_ops.MUV_TexLockIntrStop.bl_idname, icon="PLUGIN")
class MUV_WSUVMenu(bpy.types.Menu):
"""
Menu class: Master menu of world scale UV
"""
bl_idname = "uv.muv_wsuv_menu"
bl_label = "World Scale UV"
bl_description = ""
def draw(self, _):
self.layout.operator(
muv_wsuv_ops.MUV_WSUVMeasure.bl_idname, icon="PLUGIN")
self.layout.operator(
muv_wsuv_ops.MUV_WSUVApply.bl_idname, icon="PLUGIN")

View File

@ -0,0 +1,152 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Keith (Wahooney) Boshoff, Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
from bpy.props import EnumProperty, FloatProperty
import bmesh
from mathutils import Vector
from . import muv_common
class MUV_MirrorUV(bpy.types.Operator):
"""
Operation class: Mirror UV
"""
bl_idname = "uv.muv_mirror_uv"
bl_label = "Mirror UV"
bl_options = {'REGISTER', 'UNDO'}
axis = EnumProperty(
items=(
('X', "X", "Mirror Along X axis"),
('Y', "Y", "Mirror Along Y axis"),
('Z', "Z", "Mirror Along Z axis")
),
name="Axis",
description="Mirror Axis",
default='X'
)
error = FloatProperty(
name="Error",
description="Error threshold",
default=0.001,
min=0.0,
max=100.0,
soft_min=0.0,
soft_max=1.0
)
def __is_vector_similar(self, v1, v2, error):
"""
Check if two vectors are similar, within an error threshold
"""
within_err_x = abs(v2.x - v1.x) < error
within_err_y = abs(v2.y - v1.y) < error
within_err_z = abs(v2.z - v1.z) < error
return within_err_x and within_err_y and within_err_z
def __mirror_uvs(self, uv_layer, src, dst, axis, error):
"""
Copy UV coordinates from one UV face to another
"""
for sl in src.loops:
suv = sl[uv_layer].uv.copy()
svco = sl.vert.co.copy()
for dl in dst.loops:
dvco = dl.vert.co.copy()
if axis == 'X':
dvco.x = -dvco.x
elif axis == 'Y':
dvco.y = -dvco.y
elif axis == 'Z':
dvco.z = -dvco.z
if self.__is_vector_similar(svco, dvco, error):
dl[uv_layer].uv = suv.copy()
def __get_face_center(self, face):
"""
Get center coordinate of the face
"""
center = Vector((0.0, 0.0, 0.0))
for v in face.verts:
center = center + v.co
return center / len(face.verts)
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == 'MESH'
def execute(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
error = self.error
axis = self.axis
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
faces = [f for f in bm.faces if f.select]
for f_dst in faces:
count = len(f_dst.verts)
for f_src in bm.faces:
# check if this is a candidate to do mirror UV
if f_src.index == f_dst.index:
continue
if count != len(f_src.verts):
continue
# test if the vertices x values are the same sign
dst = self.__get_face_center(f_dst)
src = self.__get_face_center(f_src)
if (dst.x > 0 and src.x > 0) or (dst.x < 0 and src.x < 0):
continue
# invert source axis
if axis == 'X':
src.x = -src.x
elif axis == 'Y':
src.y = -src.z
elif axis == 'Z':
src.z = -src.z
# do mirror UV
if self.__is_vector_similar(dst, src, error):
self.__mirror_uvs(
uv_layer, f_src, f_dst, self.axis, self.error)
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}

126
uv_magic_uv/muv_mvuv_ops.py Normal file
View File

@ -0,0 +1,126 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "kgeogeo, mem, Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
import bmesh
from mathutils import Vector
class MUV_MVUV(bpy.types.Operator):
"""
Operator class: Move UV from View3D
"""
bl_idname = "view3d.muv_mvuv"
bl_label = "Move the UV from View3D"
bl_options = {'REGISTER', 'UNDO'}
def __init__(self):
self.__topology_dict = []
self.__prev_mouse = Vector((0.0, 0.0))
self.__offset_uv = Vector((0.0, 0.0))
self.__prev_offset_uv = Vector((0.0, 0.0))
self.__first_time = True
self.__ini_uvs = []
self.__running = False
def __find_uv(self, context):
bm = bmesh.from_edit_mesh(context.object.data)
topology_dict = []
uvs = []
active_uv = bm.loops.layers.uv.active
for fidx, f in enumerate(bm.faces):
for vidx, v in enumerate(f.verts):
if v.select:
uvs.append(f.loops[vidx][active_uv].uv.copy())
topology_dict.append([fidx, vidx])
return topology_dict, uvs
@classmethod
def poll(cls, context):
return context.edit_object
def modal(self, context, event):
if self.__first_time is True:
self.__prev_mouse = Vector((
event.mouse_region_x, event.mouse_region_y))
self.__first_time = False
return {'RUNNING_MODAL'}
# move UV
div = 10000
self.__offset_uv += Vector((
(event.mouse_region_x - self.__prev_mouse.x) / div,
(event.mouse_region_y - self.__prev_mouse.y) / div))
ouv = self.__offset_uv
pouv = self.__prev_offset_uv
vec = Vector((ouv.x - ouv.y, ouv.x + ouv.y))
dv = vec - pouv
self.__prev_offset_uv = vec
self.__prev_mouse = Vector((
event.mouse_region_x, event.mouse_region_y))
# check if operation is started
if self.__running is True:
if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
self.__running = False
return {'RUNNING_MODAL'}
# update UV
obj = context.object
bm = bmesh.from_edit_mesh(obj.data)
active_uv = bm.loops.layers.uv.active
for fidx, vidx in self.__topology_dict:
l = bm.faces[fidx].loops[vidx]
l[active_uv].uv = l[active_uv].uv + dv
bmesh.update_edit_mesh(obj.data)
# check mouse preference
if context.user_preferences.inputs.select_mouse == 'RIGHT':
confirm_btn = 'LEFTMOUSE'
cancel_btn = 'RIGHTMOUSE'
else:
confirm_btn = 'RIGHTMOUSE'
cancel_btn = 'LEFTMOUSE'
# cancelled
if event.type == cancel_btn and event.value == 'PRESS':
for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs):
bm.faces[fidx].loops[vidx][active_uv].uv = uv
return {'FINISHED'}
# confirmed
if event.type == confirm_btn and event.value == 'PRESS':
return {'FINISHED'}
return {'RUNNING_MODAL'}
def execute(self, context):
self.__first_time = True
self.__running = True
context.window_manager.modal_handler_add(self)
self.__topology_dict, self.__ini_uvs = self.__find_uv(context)
return {'RUNNING_MODAL'}

View File

@ -0,0 +1,285 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
from math import fabs
from collections import defaultdict
import bpy
import bmesh
import mathutils
from bpy.props import FloatProperty, FloatVectorProperty, BoolProperty
from mathutils import Vector
from . import muv_common
class MUV_PackUV(bpy.types.Operator):
"""
Operation class: Pack UV with same UV islands are integrated
Island matching algorithm
- Same center of UV island
- Same size of UV island
- Same number of UV
"""
bl_idname = "uv.muv_packuv"
bl_label = "Pack UV"
bl_description = "Pack UV (Same UV Islands are integrated)"
bl_options = {'REGISTER', 'UNDO'}
rotate = BoolProperty(
name="Rotate",
description="Rotate option used by default pack UV function",
default=False)
margin = FloatProperty(
name="Margin",
description="Margin used by default pack UV function",
min=0,
max=1,
default=0.001)
allowable_center_deviation = FloatVectorProperty(
name="Allowable Center Deviation",
description="Allowable center deviation to judge same UV island",
min=0.000001,
max=0.1,
default=(0.001, 0.001),
size=2)
allowable_size_deviation = FloatVectorProperty(
name="Allowable Size Deviation",
description="Allowable sizse deviation to judge same UV island",
min=0.000001,
max=0.1,
default=(0.001, 0.001),
size=2)
def __init__(self):
self.__face_to_verts = defaultdict(set)
self.__vert_to_faces = defaultdict(set)
def execute(self, _):
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
selected_faces = [f for f in bm.faces if f.select]
# create mesh database
for f in selected_faces:
for l in f.loops:
id_ = l[uv_layer].uv.to_tuple(5), l.vert.index
self.__face_to_verts[f.index].add(id_)
self.__vert_to_faces[id_].add(f.index)
# Group island
uv_island_lists = self.__get_island(bm)
island_info = self.__get_island_info(uv_layer, uv_island_lists)
num_group = self.__group_island(island_info)
loop_lists = [l for f in bm.faces for l in f.loops]
bpy.ops.mesh.select_all(action='DESELECT')
# pack UV
for gidx in range(num_group):
group = list(filter(
lambda i, idx=gidx: i['group'] == idx, island_info))
for f in group[0]['faces']:
f['face'].select = True
bmesh.update_edit_mesh(obj.data)
bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin)
# copy/paste UV among same islands
for gidx in range(num_group):
group = list(filter(
lambda i, idx=gidx: i['group'] == idx, island_info))
if len(group) <= 1:
continue
for g in group[1:]:
for (src_face, dest_face) in zip(
group[0]['sorted'], g['sorted']):
for (src_loop, dest_loop) in zip(
src_face['face'].loops, dest_face['face'].loops):
loop_lists[dest_loop.index][uv_layer].uv = loop_lists[
src_loop.index][uv_layer].uv
# restore face/UV selection
bpy.ops.uv.select_all(action='DESELECT')
bpy.ops.mesh.select_all(action='DESELECT')
for f in selected_faces:
f.select = True
bpy.ops.uv.select_all(action='SELECT')
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}
def __sort_island_faces(self, kd, uvs, isl1, isl2):
"""
Sort faces in island
"""
sorted_faces = []
for f in isl1['sorted']:
_, idx, _ = kd.find(
Vector((f['ave_uv'].x, f['ave_uv'].y, 0.0)))
sorted_faces.append(isl2['faces'][uvs[idx]['face_idx']])
return sorted_faces
def __group_island(self, island_info):
"""
Group island
"""
num_group = 0
while True:
# search islands which is not parsed yet
isl_1 = None
for isl_1 in island_info:
if isl_1['group'] == -1:
break
else:
break # all faces are parsed
if isl_1 is None:
break
isl_1['group'] = num_group
isl_1['sorted'] = isl_1['faces']
# search same island
for isl_2 in island_info:
if isl_2['group'] == -1:
dcx = isl_2['center'].x - isl_1['center'].x
dcy = isl_2['center'].y - isl_1['center'].y
dsx = isl_2['size'].x - isl_1['size'].x
dsy = isl_2['size'].y - isl_1['size'].y
center_x_matched = (
fabs(dcx) < self.allowable_center_deviation[0])
center_y_matched = (
fabs(dcy) < self.allowable_center_deviation[1])
size_x_matched = (
fabs(dsx) < self.allowable_size_deviation[0])
size_y_matched = (
fabs(dsy) < self.allowable_size_deviation[1])
center_matched = center_x_matched and center_y_matched
size_matched = size_x_matched and size_y_matched
num_uv_matched = (isl_2['num_uv'] == isl_1['num_uv'])
# are islands have same?
if center_matched and size_matched and num_uv_matched:
isl_2['group'] = num_group
kd = mathutils.kdtree.KDTree(len(isl_2['faces']))
uvs = [
{
'uv': Vector(
(f['ave_uv'].x, f['ave_uv'].y, 0.0)
),
'face_idx': fidx
} for fidx, f in enumerate(isl_2['faces'])
]
for i, uv in enumerate(uvs):
kd.insert(uv['uv'], i)
kd.balance()
# sort faces for copy/paste UV
isl_2['sorted'] = self.__sort_island_faces(
kd, uvs, isl_1, isl_2)
num_group = num_group + 1
return num_group
def __get_island_info(self, uv_layer, islands):
"""
get information about each island
"""
island_info = []
for isl in islands:
info = {}
max_uv = Vector((-10000000.0, -10000000.0))
min_uv = Vector((10000000.0, 10000000.0))
ave_uv = Vector((0.0, 0.0))
num_uv = 0
for face in isl:
n = 0
a = Vector((0.0, 0.0))
for l in face['face'].loops:
uv = l[uv_layer].uv
if uv.x > max_uv.x:
max_uv.x = uv.x
if uv.y > max_uv.y:
max_uv.y = uv.y
if uv.x < min_uv.x:
min_uv.x = uv.x
if uv.y < min_uv.y:
min_uv.y = uv.y
a = a + uv
n = n + 1
ave_uv = ave_uv + a
num_uv = num_uv + n
a = a / n
face['ave_uv'] = a
ave_uv = ave_uv / num_uv
info['center'] = ave_uv
info['size'] = max_uv - min_uv
info['num_uv'] = num_uv
info['group'] = -1
info['faces'] = isl
island_info.append(info)
return island_info
def __parse_island(self, bm, face_idx, faces_left, island):
"""
Parse island
"""
if face_idx in faces_left:
faces_left.remove(face_idx)
island.append({'face': bm.faces[face_idx]})
for v in self.__face_to_verts[face_idx]:
connected_faces = self.__vert_to_faces[v]
if connected_faces:
for cf in connected_faces:
self.__parse_island(bm, cf, faces_left, island)
def __get_island(self, bm):
"""
Get island list
"""
uv_island_lists = []
faces_left = set(self.__face_to_verts.keys())
while len(faces_left) > 0:
current_island = []
face_idx = list(faces_left)[0]
self.__parse_island(bm, face_idx, faces_left, current_island)
uv_island_lists.append(current_island)
return uv_island_lists

View File

@ -0,0 +1,141 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
from bpy.props import BoolProperty, FloatProperty, FloatVectorProperty
from bpy.types import AddonPreferences
class MUV_Preferences(AddonPreferences):
"""Preferences class: Preferences for this add-on"""
bl_idname = __package__
# enable/disable switcher
enable_texproj = BoolProperty(
name="Texture Projection",
default=True)
enable_uvbb = BoolProperty(
name="Bounding Box",
default=True)
# for Texture Projection
texproj_canvas_padding = FloatVectorProperty(
name="Canvas Padding",
description="Canvas Padding.",
size=2,
max=50.0,
min=0.0,
default=(20.0, 20.0))
# for UV Bounding Box
uvbb_cp_size = FloatProperty(
name="Size",
description="Control Point Size",
default=6.0,
min=3.0,
max=100.0)
uvbb_cp_react_size = FloatProperty(
name="React Size",
description="Size event fired",
default=10.0,
min=3.0,
max=100.0)
def draw(self, _):
layout = self.layout
layout.label("Switch Enable/Disable and Configurate Features:")
layout.prop(self, "enable_texproj")
if self.enable_texproj:
sp = layout.split(percentage=0.05)
col = sp.column() # spacer
sp = sp.split(percentage=0.3)
col = sp.column()
col.label("Texture Display: ")
col.prop(self, "texproj_canvas_padding")
layout.prop(self, "enable_uvbb")
if self.enable_uvbb:
sp = layout.split(percentage=0.05)
col = sp.column() # spacer
sp = sp.split(percentage=0.3)
col = sp.column()
col.label("Control Point: ")
col.prop(self, "uvbb_cp_size")
col.prop(self, "uvbb_cp_react_size")
layout.label("Description:")
column = layout.column(align=True)
column.label("Magic UV is composed of many UV editing features.")
column.label("See tutorial page if you know about this add-on.")
column.label("https://github.com/nutti/Magic-UV/wiki/Tutorial")
layout.label("Location:")
row = layout.row(align=True)
sp = row.split(percentage=0.3)
sp.label("View3D > U")
sp = sp.split(percentage=1.0)
col = sp.column(align=True)
col.label("Copy/Paste UV Coordinates")
col.label("Copy/Paste UV Coordinates (by selection sequence)")
col.label("Flip/Rotate UVs")
col.label("Transfer UV")
col.label("Move UV from 3D View")
col.label("Texture Lock")
col.label("Mirror UV")
col.label("World Scale UV")
col.label("Unwrap Constraint")
col.label("Preserve UV Aspect")
row = layout.row(align=True)
sp = row.split(percentage=0.3)
sp.label("View3D > Object")
sp = sp.split(percentage=1.0)
col = sp.column(align=True)
col.label("Copy/Paste UV Coordinates (Among same objects)")
row = layout.row(align=True)
sp = row.split(percentage=0.3)
sp.label("ImageEditor > Property Panel")
sp = sp.split(percentage=1.0)
col = sp.column(align=True)
col.label("Manipulate UV with Bounding Box in UV Editor")
row = layout.row(align=True)
sp = row.split(percentage=0.3)
sp.label("View3D > Property Panel")
sp = sp.split(percentage=1.0)
col = sp.column(align=True)
col.label("Texture Projection")
row = layout.row(align=True)
sp = row.split(percentage=0.3)
sp.label("ImageEditor > UVs")
sp = sp.split(percentage=1.0)
col = sp.column(align=True)
col.label("Pack UV (with same UV island packing)")

View File

@ -0,0 +1,119 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
import bmesh
from bpy.props import StringProperty
from mathutils import Vector
from . import muv_common
class MUV_PreserveUVAspect(bpy.types.Operator):
"""
Operation class: Preserve UV Aspect
"""
bl_idname = "uv.muv_preserve_uv_aspect"
bl_label = "Preserve UV Aspect"
bl_options = {'REGISTER', 'UNDO'}
dest_img_name = StringProperty(options={'HIDDEN'})
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == 'MESH'
def execute(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
tex_layer = bm.faces.layers.tex.verify()
sel_faces = [f for f in bm.faces if f.select]
dest_img = bpy.data.images[self.dest_img_name]
info = {}
for f in sel_faces:
if not f[tex_layer].image in info.keys():
info[f[tex_layer].image] = {}
info[f[tex_layer].image]['faces'] = []
info[f[tex_layer].image]['faces'].append(f)
for img in info:
src_img = img
ratio = Vector((
dest_img.size[0] / src_img.size[0],
dest_img.size[1] / src_img.size[1]))
origin = Vector((100000.0, 100000.0))
for f in info[img]['faces']:
for l in f.loops:
uv = l[uv_layer].uv
origin.x = min(uv.x, origin.x)
origin.y = min(uv.y, origin.y)
info[img]['ratio'] = ratio
info[img]['origin'] = origin
for img in info:
for f in info[img]['faces']:
f[tex_layer].image = dest_img
for l in f.loops:
uv = l[uv_layer].uv
diff = uv - info[img]['origin']
diff.x = diff.x / info[img]['ratio'].x
diff.y = diff.y / info[img]['ratio'].y
uv.x = origin.x + diff.x
uv.y = origin.y + diff.y
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}
class MUV_PreserveUVAspectMenu(bpy.types.Menu):
"""
Menu class: Preserve UV Aspect
"""
bl_idname = "uv.muv_preserve_uv_aspect_menu"
bl_label = "Preserve UV Aspect"
bl_description = "Preserve UV Aspect"
def draw(self, _):
layout = self.layout
# create sub menu
for key in bpy.data.images.keys():
layout.operator(
MUV_PreserveUVAspect.bl_idname,
text=key, icon="PLUGIN").dest_img_name = key

143
uv_magic_uv/muv_props.py Normal file
View File

@ -0,0 +1,143 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
from bpy.props import FloatProperty, EnumProperty, BoolProperty
DEBUG = False
def get_loaded_texture_name(_, __):
items = [(key, key, "") for key in bpy.data.images.keys()]
items.append(("None", "None", ""))
return items
# Properties used in this add-on.
class MUV_Properties():
cpuv = None
cpuv_obj = None
cpuv_selseq = None
transuv = None
uvbb = None
texproj = None
texlock = None
texwrap = None
wsuv = None
def __init__(self):
self.cpuv = MUV_CPUVProps()
self.cpuv_obj = MUV_CPUVProps()
self.cpuv_selseq = MUV_CPUVSelSeqProps()
self.transuv = MUV_TransUVProps()
self.uvbb = MUV_UVBBProps()
self.texproj = MUV_TexProjProps()
self.texlock = MUV_TexLockProps()
self.texwrap = MUV_TexWrapProps()
self.wsuv = MUV_WSUVProps()
class MUV_CPUVProps():
src_uvs = []
src_pin_uvs = []
class MUV_CPUVSelSeqProps():
src_uvs = []
src_pin_uvs = []
class MUV_TransUVProps():
topology_copied = []
class MUV_UVBBProps():
uv_info_ini = []
ctrl_points_ini = []
ctrl_points = []
running = False
class MUV_TexProjProps():
running = False
class MUV_TexLockProps():
verts_orig = None
intr_verts_orig = None
intr_running = False
class MUV_TexWrapProps():
src_face_index = -1
class MUV_WSUVProps():
ref_sv = None
ref_suv = None
def init_props(scene):
scene.muv_props = MUV_Properties()
scene.muv_uvbb_uniform_scaling = BoolProperty(
name="Uniform Scaling",
description="Enable Uniform Scaling",
default=False)
scene.muv_texproj_tex_magnitude = FloatProperty(
name="Magnitude",
description="Texture Magnitude.",
default=0.5,
min=0.0,
max=100.0)
scene.muv_texproj_tex_image = EnumProperty(
name="Image",
description="Texture Image.",
items=get_loaded_texture_name)
scene.muv_texproj_tex_transparency = FloatProperty(
name="Transparency",
description="Texture Transparency.",
default=0.2,
min=0.0,
max=1.0)
scene.muv_texproj_adjust_window = BoolProperty(
name="Adjust Window",
description="Size of renderered texture is fitted to window.",
default=True)
scene.muv_texproj_apply_tex_aspect = BoolProperty(
name="Texture Aspect Ratio",
description="Apply Texture Aspect ratio to displayed texture.",
default=True)
def clear_props(scene):
del scene.muv_props
del scene.muv_uvbb_uniform_scaling
del scene.muv_texproj_tex_magnitude
del scene.muv_texproj_tex_image
del scene.muv_texproj_tex_transparency
del scene.muv_texproj_adjust_window
del scene.muv_texproj_apply_tex_aspect

View File

@ -0,0 +1,431 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import math
from math import atan2, cos, sqrt, sin, fabs
import bpy
import bmesh
from mathutils import Vector
from bpy.props import BoolProperty
from . import muv_common
def get_vco(verts_orig, loop):
"""
Get vertex original coordinate from loop
"""
for vo in verts_orig:
if vo["vidx"] == loop.vert.index and vo["moved"] is False:
return vo["vco"]
return loop.vert.co
def get_link_loops(vert):
"""
Get loop linked to vertex
"""
link_loops = []
for f in vert.link_faces:
adj_loops = []
for loop in f.loops:
# self loop
if loop.vert == vert:
l = loop
# linked loop
else:
for e in loop.vert.link_edges:
if e.other_vert(loop.vert) == vert:
adj_loops.append(loop)
if len(adj_loops) < 2:
return None
link_loops.append({"l": l, "l0": adj_loops[0], "l1": adj_loops[1]})
return link_loops
def get_ini_geom(link_loop, uv_layer, verts_orig, v_orig):
"""
Get initial geometory
(Get interior angle of face in vertex/UV space)
"""
u = link_loop["l"][uv_layer].uv
v0 = get_vco(verts_orig, link_loop["l0"])
u0 = link_loop["l0"][uv_layer].uv
v1 = get_vco(verts_orig, link_loop["l1"])
u1 = link_loop["l1"][uv_layer].uv
# get interior angle of face in vertex space
v0v1 = v1 - v0
v0v = v_orig["vco"] - v0
v1v = v_orig["vco"] - v1
theta0 = v0v1.angle(v0v)
theta1 = v0v1.angle(-v1v)
if (theta0 + theta1) > math.pi:
theta0 = v0v1.angle(-v0v)
theta1 = v0v1.angle(v1v)
# get interior angle of face in UV space
u0u1 = u1 - u0
u0u = u - u0
u1u = u - u1
phi0 = u0u1.angle(u0u)
phi1 = u0u1.angle(-u1u)
if (phi0 + phi1) > math.pi:
phi0 = u0u1.angle(-u0u)
phi1 = u0u1.angle(u1u)
# get direction of linked UV coordinate
# this will be used to judge whether angle is more or less than 180 degree
dir0 = u0u1.cross(u0u) > 0
dir1 = u0u1.cross(u1u) > 0
return {
"theta0": theta0,
"theta1": theta1,
"phi0": phi0,
"phi1": phi1,
"dir0": dir0,
"dir1": dir1}
def get_target_uv(link_loop, uv_layer, verts_orig, v, ini_geom):
"""
Get target UV coordinate
"""
v0 = get_vco(verts_orig, link_loop["l0"])
lo0 = link_loop["l0"]
v1 = get_vco(verts_orig, link_loop["l1"])
lo1 = link_loop["l1"]
# get interior angle of face in vertex space
v0v1 = v1 - v0
v0v = v.co - v0
v1v = v.co - v1
theta0 = v0v1.angle(v0v)
theta1 = v0v1.angle(-v1v)
if (theta0 + theta1) > math.pi:
theta0 = v0v1.angle(-v0v)
theta1 = v0v1.angle(v1v)
# calculate target interior angle in UV space
phi0 = theta0 * ini_geom["phi0"] / ini_geom["theta0"]
phi1 = theta1 * ini_geom["phi1"] / ini_geom["theta1"]
uv0 = lo0[uv_layer].uv
uv1 = lo1[uv_layer].uv
# calculate target vertex coordinate from target interior angle
tuv0, tuv1 = calc_tri_vert(uv0, uv1, phi0, phi1)
# target UV coordinate depends on direction, so judge using direction of
# linked UV coordinate
u0u1 = uv1 - uv0
u0u = tuv0 - uv0
u1u = tuv0 - uv1
dir0 = u0u1.cross(u0u) > 0
dir1 = u0u1.cross(u1u) > 0
if (ini_geom["dir0"] != dir0) or (ini_geom["dir1"] != dir1):
return tuv1
return tuv0
def calc_tri_vert(v0, v1, angle0, angle1):
"""
Calculate rest coordinate from other coordinates and angle of end
"""
angle = math.pi - angle0 - angle1
alpha = atan2(v1.y - v0.y, v1.x - v0.x)
d = (v1.x - v0.x) / cos(alpha)
a = d * sin(angle0) / sin(angle)
b = d * sin(angle1) / sin(angle)
s = (a + b + d) / 2.0
if fabs(d) < 0.0000001:
xd = 0
yd = 0
else:
xd = (b * b - a * a + d * d) / (2 * d)
yd = 2 * sqrt(s * (s - a) * (s - b) * (s - d)) / d
x1 = xd * cos(alpha) - yd * sin(alpha) + v0.x
y1 = xd * sin(alpha) + yd * cos(alpha) + v0.y
x2 = xd * cos(alpha) + yd * sin(alpha) + v0.x
y2 = xd * sin(alpha) - yd * cos(alpha) + v0.y
return Vector((x1, y1)), Vector((x2, y2))
class MUV_TexLockStart(bpy.types.Operator):
"""
Operation class: Start Texture Lock
"""
bl_idname = "uv.muv_texlock_start"
bl_label = "Start"
bl_description = "Start Texture Lock"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.muv_props.texlock
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report(
{'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
props.verts_orig = [
{"vidx": v.index, "vco": v.co.copy(), "moved": False}
for v in bm.verts if v.select]
return {'FINISHED'}
class MUV_TexLockStop(bpy.types.Operator):
"""
Operation class: Stop Texture Lock
"""
bl_idname = "uv.muv_texlock_stop"
bl_label = "Stop"
bl_description = "Start Texture Lock"
bl_options = {'REGISTER', 'UNDO'}
connect = BoolProperty(
name="Connect UV",
default=True)
def execute(self, context):
props = context.scene.muv_props.texlock
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report(
{'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
verts = [v.index for v in bm.verts if v.select]
verts_orig = props.verts_orig
# move UV followed by vertex coordinate
for vidx, v_orig in zip(verts, verts_orig):
if vidx != v_orig["vidx"]:
self.report({'ERROR'}, "Internal Error")
return {"CANCELLED"}
v = bm.verts[vidx]
link_loops = get_link_loops(v)
result = []
for ll in link_loops:
ini_geom = get_ini_geom(ll, uv_layer, verts_orig, v_orig)
target_uv = get_target_uv(
ll, uv_layer, verts_orig, v, ini_geom)
result.append({"l": ll["l"], "uv": target_uv})
# connect other face's UV
if self.connect:
ave = Vector((0.0, 0.0))
for r in result:
ave = ave + r["uv"]
ave = ave / len(result)
for r in result:
r["l"][uv_layer].uv = ave
else:
for r in result:
r["l"][uv_layer].uv = r["uv"]
v_orig["moved"] = True
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}
class MUV_TexLockUpdater(bpy.types.Operator):
"""
Operation class: Texture locking updater
"""
bl_idname = "uv.muv_texlock_updater"
bl_label = "Texture Lock Updater"
bl_description = "Texture Lock Updater"
def __init__(self):
self.__timer = None
def __update_uv(self, context):
"""
Update UV when vertex coordinates are changed
"""
props = context.scene.muv_props.texlock
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
verts = [v.index for v in bm.verts if v.select]
verts_orig = props.intr_verts_orig
for vidx, v_orig in zip(verts, verts_orig):
if vidx != v_orig["vidx"]:
self.report({'ERROR'}, "Internal Error")
return {"CANCELLED"}
v = bm.verts[vidx]
link_loops = get_link_loops(v)
result = []
for ll in link_loops:
ini_geom = get_ini_geom(ll, uv_layer, verts_orig, v_orig)
target_uv = get_target_uv(
ll, uv_layer, verts_orig, v, ini_geom)
result.append({"l": ll["l"], "uv": target_uv})
# UV connect option is always true, because it raises
# unexpected behavior
ave = Vector((0.0, 0.0))
for r in result:
ave = ave + r["uv"]
ave = ave / len(result)
for r in result:
r["l"][uv_layer].uv = ave
v_orig["moved"] = True
bmesh.update_edit_mesh(obj.data)
muv_common.redraw_all_areas()
props.intr_verts_orig = [
{"vidx": v.index, "vco": v.co.copy(), "moved": False}
for v in bm.verts if v.select]
def modal(self, context, event):
props = context.scene.muv_props.texlock
if context.area:
context.area.tag_redraw()
if props.intr_running is False:
self.__handle_remove(context)
return {'FINISHED'}
if event.type == 'TIMER':
self.__update_uv(context)
return {'PASS_THROUGH'}
def __handle_add(self, context):
if self.__timer is None:
self.__timer = context.window_manager.event_timer_add(
0.10, context.window)
context.window_manager.modal_handler_add(self)
def __handle_remove(self, context):
if self.__timer is not None:
context.window_manager.event_timer_remove(self.__timer)
self.__timer = None
def execute(self, context):
props = context.scene.muv_props.texlock
if props.intr_running is False:
self.__handle_add(context)
props.intr_running = True
return {'RUNNING_MODAL'}
else:
props.intr_running = False
if context.area:
context.area.tag_redraw()
return {'FINISHED'}
class MUV_TexLockIntrStart(bpy.types.Operator):
"""
Operation class: Start texture locking (Interactive mode)
"""
bl_idname = "uv.muv_texlock_intr_start"
bl_label = "Texture Lock Start (Interactive mode)"
bl_description = "Texture Lock Start (Realtime UV update)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.muv_props.texlock
if props.intr_running is True:
return {'CANCELLED'}
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
props.intr_verts_orig = [
{"vidx": v.index, "vco": v.co.copy(), "moved": False}
for v in bm.verts if v.select]
bpy.ops.uv.muv_texlock_updater()
return {'FINISHED'}
# Texture lock (Stop, Interactive mode)
class MUV_TexLockIntrStop(bpy.types.Operator):
"""
Operation class: Stop texture locking (interactive mode)
"""
bl_idname = "uv.muv_texlock_intr_stop"
bl_label = "Texture Lock Stop (Interactive mode)"
bl_description = "Texture Lock Stop (Realtime UV update)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.muv_props.texlock
if props.intr_running is False:
return {'CANCELLED'}
bpy.ops.uv.muv_texlock_updater()
return {'FINISHED'}

View File

@ -0,0 +1,328 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
from collections import namedtuple
import bpy
import bgl
import bmesh
import mathutils
from bpy_extras import view3d_utils
from . import muv_common
Rect = namedtuple('Rect', 'x0 y0 x1 y1')
Rect2 = namedtuple('Rect2', 'x y width height')
def get_canvas(context, magnitude):
"""
Get canvas to be renderred texture
"""
sc = context.scene
prefs = context.user_preferences.addons["uv_magic_uv"].preferences
region_w = context.region.width
region_h = context.region.height
canvas_w = region_w - prefs.texproj_canvas_padding[0] * 2.0
canvas_h = region_h - prefs.texproj_canvas_padding[1] * 2.0
img = bpy.data.images[sc.muv_texproj_tex_image]
tex_w = img.size[0]
tex_h = img.size[1]
center_x = region_w * 0.5
center_y = region_h * 0.5
if sc.muv_texproj_adjust_window:
ratio_x = canvas_w / tex_w
ratio_y = canvas_h / tex_h
if sc.muv_texproj_apply_tex_aspect:
ratio = ratio_y if ratio_x > ratio_y else ratio_x
len_x = ratio * tex_w
len_y = ratio * tex_h
else:
len_x = canvas_w
len_y = canvas_h
else:
if sc.muv_texproj_apply_tex_aspect:
len_x = tex_w * magnitude
len_y = tex_h * magnitude
else:
len_x = region_w * magnitude
len_y = region_h * magnitude
x0 = int(center_x - len_x * 0.5)
y0 = int(center_y - len_y * 0.5)
x1 = int(center_x + len_x * 0.5)
y1 = int(center_y + len_y * 0.5)
return Rect(x0, y0, x1, y1)
def rect_to_rect2(rect):
"""
Convert Rect1 to Rect2
"""
return Rect2(rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0)
def region_to_canvas(rg_vec, canvas):
"""
Convert screen region to canvas
"""
cv_rect = rect_to_rect2(canvas)
cv_vec = mathutils.Vector()
cv_vec.x = (rg_vec.x - cv_rect.x) / cv_rect.width
cv_vec.y = (rg_vec.y - cv_rect.y) / cv_rect.height
return cv_vec
class MUV_TexProjRenderer(bpy.types.Operator):
"""
Operation class: Render selected texture
No operation (only rendering texture)
"""
bl_idname = "uv.muv_texproj_renderer"
bl_description = "Render selected texture"
bl_label = "Texture renderer"
__handle = None
@staticmethod
def handle_add(obj, context):
MUV_TexProjRenderer.__handle = bpy.types.SpaceView3D.draw_handler_add(
MUV_TexProjRenderer.draw_texture,
(obj, context), 'WINDOW', 'POST_PIXEL')
@staticmethod
def handle_remove():
if MUV_TexProjRenderer.__handle is not None:
bpy.types.SpaceView3D.draw_handler_remove(
MUV_TexProjRenderer.__handle, 'WINDOW')
MUV_TexProjRenderer.__handle = None
@staticmethod
def draw_texture(_, context):
sc = context.scene
# no textures are selected
if sc.muv_texproj_tex_image == "None":
return
# get texture to be renderred
img = bpy.data.images[sc.muv_texproj_tex_image]
# setup rendering region
rect = get_canvas(context, sc.muv_texproj_tex_magnitude)
positions = [
[rect.x0, rect.y0],
[rect.x0, rect.y1],
[rect.x1, rect.y1],
[rect.x1, rect.y0]
]
tex_coords = [
[0.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[1.0, 0.0]
]
# OpenGL configuration
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_TEXTURE_2D)
if img.bindcode:
bind = img.bindcode[0]
bgl.glBindTexture(bgl.GL_TEXTURE_2D, bind)
bgl.glTexParameteri(
bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_LINEAR)
bgl.glTexParameteri(
bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_LINEAR)
bgl.glTexEnvi(
bgl.GL_TEXTURE_ENV, bgl.GL_TEXTURE_ENV_MODE, bgl.GL_MODULATE)
# render texture
bgl.glBegin(bgl.GL_QUADS)
bgl.glColor4f(1.0, 1.0, 1.0, sc.muv_texproj_tex_transparency)
for (v1, v2), (u, v) in zip(positions, tex_coords):
bgl.glTexCoord2f(u, v)
bgl.glVertex2f(v1, v2)
bgl.glEnd()
class MUV_TexProjStart(bpy.types.Operator):
"""
Operation class: Start Texture Projection
"""
bl_idname = "uv.muv_texproj_start"
bl_label = "Start Texture Projection"
bl_description = "Start Texture Projection"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.muv_props.texproj
if props.running is False:
MUV_TexProjRenderer.handle_add(self, context)
props.running = True
if context.area:
context.area.tag_redraw()
return {'FINISHED'}
class MUV_TexProjStop(bpy.types.Operator):
"""
Operation class: Stop Texture Projection
"""
bl_idname = "uv.muv_texproj_stop"
bl_label = "Stop Texture Projection"
bl_description = "Stop Texture Projection"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.muv_props.texproj
if props.running is True:
MUV_TexProjRenderer.handle_remove()
props.running = False
if context.area:
context.area.tag_redraw()
return {'FINISHED'}
class MUV_TexProjProject(bpy.types.Operator):
"""
Operation class: Project texture
"""
bl_idname = "uv.muv_texproj_project"
bl_label = "Project Texture"
bl_description = "Project Texture"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
sc = context.scene
if sc.muv_texproj_tex_image == "None":
self.report({'WARNING'}, "No textures are selected")
return {'CANCELLED'}
_, region, space = muv_common.get_space(
'VIEW_3D', 'WINDOW', 'VIEW_3D')
# get faces to be texture projected
obj = context.active_object
world_mat = obj.matrix_world
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
# get UV and texture layer
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
tex_layer = bm.faces.layers.tex.verify()
sel_faces = [f for f in bm.faces if f.select]
# transform 3d space to screen region
v_screen = [
view3d_utils.location_3d_to_region_2d(
region,
space.region_3d,
world_mat * l.vert.co)
for f in sel_faces for l in f.loops
]
# transform screen region to canvas
v_canvas = [
region_to_canvas(
v,
get_canvas(bpy.context, sc.muv_texproj_tex_magnitude))
for v in v_screen
]
# project texture to object
i = 0
for f in sel_faces:
f[tex_layer].image = bpy.data.images[sc.muv_texproj_tex_image]
for l in f.loops:
l[uv_layer].uv = v_canvas[i].to_2d()
i = i + 1
muv_common.redraw_all_areas()
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}
class OBJECT_PT_TP(bpy.types.Panel):
"""
Panel class: Texture Projection Menu on Property Panel on View3D
"""
bl_label = "Texture Projection"
bl_description = "Texture Projection Menu"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_context = 'mesh_edit'
@classmethod
def poll(cls, context):
prefs = context.user_preferences.addons["uv_magic_uv"].preferences
return prefs.enable_texproj
def draw_header(self, _):
layout = self.layout
layout.label(text="", icon='PLUGIN')
def draw(self, context):
sc = context.scene
layout = self.layout
props = sc.muv_props.texproj
if props.running is False:
layout.operator(
MUV_TexProjStart.bl_idname, text="Start", icon='PLAY')
else:
layout.operator(
MUV_TexProjStop.bl_idname, text="Stop", icon='PAUSE')
layout.prop(sc, "muv_texproj_tex_image", text="Image")
layout.prop(
sc, "muv_texproj_tex_transparency", text="Transparency"
)
layout.prop(sc, "muv_texproj_adjust_window", text="Adjust Window")
if not sc.muv_texproj_adjust_window:
layout.prop(sc, "muv_texproj_tex_magnitude", text="Magnitude")
layout.prop(
sc, "muv_texproj_apply_tex_aspect", text="Texture Aspect Ratio"
)
layout.operator(MUV_TexProjProject.bl_idname, text="Project")

View File

@ -0,0 +1,345 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>, Mifth, MaxRobinot"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
from collections import OrderedDict
import bpy
import bmesh
from bpy.props import BoolProperty
from . import muv_props
from . import muv_common
class MUV_TransUVCopy(bpy.types.Operator):
"""
Operation class: Transfer UV copy
Topological based copy
"""
bl_idname = "uv.muv_transuv_copy"
bl_label = "Transfer UV Copy"
bl_description = "Transfer UV Copy (Topological based copy)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.muv_props.transuv
active_obj = context.scene.objects.active
bm = bmesh.from_edit_mesh(active_obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
# get UV layer
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
props.topology_copied.clear()
# get selected faces
active_face = bm.faces.active
sel_faces = [face for face in bm.faces if face.select]
if len(sel_faces) != 2:
self.report({'WARNING'}, "Two faces must be selected")
return {'CANCELLED'}
if not active_face or active_face not in sel_faces:
self.report({'WARNING'}, "Two faces must be active")
return {'CANCELLED'}
# parse all faces according to selection
active_face_nor = active_face.normal.copy()
all_sorted_faces = main_parse(
self, uv_layer, sel_faces, active_face,
active_face_nor)
if all_sorted_faces:
for face_data in all_sorted_faces.values():
uv_loops = face_data[2]
uvs = [l.uv.copy() for l in uv_loops]
pin_uvs = [l.pin_uv for l in uv_loops]
props.topology_copied.append([uvs, pin_uvs])
bmesh.update_edit_mesh(active_obj.data)
return {'FINISHED'}
class MUV_TransUVPaste(bpy.types.Operator):
"""
Operation class: Transfer UV paste
Topological based paste
"""
bl_idname = "uv.muv_transuv_paste"
bl_label = "Transfer UV Paste"
bl_description = "Transfer UV Paste (Topological based paste)"
bl_options = {'REGISTER', 'UNDO'}
invert_normals = BoolProperty(
name="Invert Normals",
description="Invert Normals",
default=False)
def execute(self, context):
props = context.scene.muv_props.transuv
active_obj = context.scene.objects.active
bm = bmesh.from_edit_mesh(active_obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
# get UV layer
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
# get selection history
all_sel_faces = [
e for e in bm.select_history
if isinstance(e, bmesh.types.BMFace) and e.select]
if len(all_sel_faces) % 2 != 0:
self.report({'WARNING'}, "Two faces must be selected")
return {'CANCELLED'}
# parse selection history
for i, _ in enumerate(all_sel_faces):
if i > 0 and i % 2 != 0:
sel_faces = [all_sel_faces[i - 1], all_sel_faces[i]]
active_face = all_sel_faces[i]
# parse all faces according to selection history
active_face_nor = active_face.normal.copy()
if self.invert_normals:
active_face_nor.negate()
all_sorted_faces = main_parse(
self, uv_layer, sel_faces, active_face,
active_face_nor)
if all_sorted_faces:
# check amount of copied/pasted faces
if len(all_sorted_faces) != len(props.topology_copied):
self.report(
{'WARNING'},
"Mesh has different amount of faces"
)
return {'FINISHED'}
for i, face_data in enumerate(all_sorted_faces.values()):
copied_data = props.topology_copied[i]
# check amount of copied/pasted verts
if len(copied_data[0]) != len(face_data[2]):
bpy.ops.mesh.select_all(action='DESELECT')
# select problematic face
list(all_sorted_faces.keys())[i].select = True
self.report(
{'WARNING'},
"Face have different amount of vertices"
)
return {'FINISHED'}
for j, uvloop in enumerate(face_data[2]):
uvloop.uv = copied_data[0][j]
uvloop.pin_uv = copied_data[1][j]
bmesh.update_edit_mesh(active_obj.data)
return {'FINISHED'}
def main_parse(
self, uv_layer, sel_faces,
active_face, active_face_nor):
all_sorted_faces = OrderedDict() # This is the main stuff
used_verts = set()
used_edges = set()
faces_to_parse = []
# get shared edge of two faces
cross_edges = []
for edge in active_face.edges:
if edge in sel_faces[0].edges and edge in sel_faces[1].edges:
cross_edges.append(edge)
# parse two selected faces
if cross_edges and len(cross_edges) == 1:
shared_edge = cross_edges[0]
vert1 = None
vert2 = None
dot_n = active_face_nor.normalized()
edge_vec_1 = (shared_edge.verts[1].co - shared_edge.verts[0].co)
edge_vec_len = edge_vec_1.length
edge_vec_1 = edge_vec_1.normalized()
af_center = active_face.calc_center_median()
af_vec = shared_edge.verts[0].co + (edge_vec_1 * (edge_vec_len * 0.5))
af_vec = (af_vec - af_center).normalized()
if af_vec.cross(edge_vec_1).dot(dot_n) > 0:
vert1 = shared_edge.verts[0]
vert2 = shared_edge.verts[1]
else:
vert1 = shared_edge.verts[1]
vert2 = shared_edge.verts[0]
# get active face stuff and uvs
face_stuff = get_other_verts_edges(
active_face, vert1, vert2, shared_edge, uv_layer)
all_sorted_faces[active_face] = face_stuff
used_verts.update(active_face.verts)
used_edges.update(active_face.edges)
# get first selected face stuff and uvs as they share shared_edge
second_face = sel_faces[0]
if second_face is active_face:
second_face = sel_faces[1]
face_stuff = get_other_verts_edges(
second_face, vert1, vert2, shared_edge, uv_layer)
all_sorted_faces[second_face] = face_stuff
used_verts.update(second_face.verts)
used_edges.update(second_face.edges)
# first Grow
faces_to_parse.append(active_face)
faces_to_parse.append(second_face)
else:
self.report({'WARNING'}, "Two faces should share one edge")
return None
# parse all faces
while True:
new_parsed_faces = []
if not faces_to_parse:
break
for face in faces_to_parse:
face_stuff = all_sorted_faces.get(face)
new_faces = parse_faces(
face, face_stuff, used_verts, used_edges, all_sorted_faces,
uv_layer)
if new_faces == 'CANCELLED':
self.report({'WARNING'}, "More than 2 faces share edge")
return None
new_parsed_faces += new_faces
faces_to_parse = new_parsed_faces
return all_sorted_faces
def parse_faces(
check_face, face_stuff, used_verts, used_edges, all_sorted_faces,
uv_layer):
"""recurse faces around the new_grow only"""
new_shared_faces = []
for sorted_edge in face_stuff[1]:
shared_faces = sorted_edge.link_faces
if shared_faces:
if len(shared_faces) > 2:
bpy.ops.mesh.select_all(action='DESELECT')
for face_sel in shared_faces:
face_sel.select = True
shared_faces = []
return 'CANCELLED'
clear_shared_faces = get_new_shared_faces(
check_face, sorted_edge, shared_faces, all_sorted_faces.keys())
if clear_shared_faces:
shared_face = clear_shared_faces[0]
# get vertices of the edge
vert1 = sorted_edge.verts[0]
vert2 = sorted_edge.verts[1]
muv_common.debug_print(face_stuff[0], vert1, vert2)
if face_stuff[0].index(vert1) > face_stuff[0].index(vert2):
vert1 = sorted_edge.verts[1]
vert2 = sorted_edge.verts[0]
muv_common.debug_print(shared_face.verts, vert1, vert2)
new_face_stuff = get_other_verts_edges(
shared_face, vert1, vert2, sorted_edge, uv_layer)
all_sorted_faces[shared_face] = new_face_stuff
used_verts.update(shared_face.verts)
used_edges.update(shared_face.edges)
if muv_props.DEBUG:
shared_face.select = True # test which faces are parsed
new_shared_faces.append(shared_face)
return new_shared_faces
def get_new_shared_faces(orig_face, shared_edge, check_faces, used_faces):
shared_faces = []
for face in check_faces:
is_shared_edge = shared_edge in face.edges
not_used = face not in used_faces
not_orig = face is not orig_face
not_hide = face.hide is False
if is_shared_edge and not_used and not_orig and not_hide:
shared_faces.append(face)
return shared_faces
def get_other_verts_edges(face, vert1, vert2, first_edge, uv_layer):
face_edges = [first_edge]
face_verts = [vert1, vert2]
face_loops = []
other_edges = [edge for edge in face.edges if edge not in face_edges]
for _ in range(len(other_edges)):
found_edge = None
# get sorted verts and edges
for edge in other_edges:
if face_verts[-1] in edge.verts:
other_vert = edge.other_vert(face_verts[-1])
if other_vert not in face_verts:
face_verts.append(other_vert)
found_edge = edge
if found_edge not in face_edges:
face_edges.append(edge)
break
other_edges.remove(found_edge)
# get sorted uvs
for vert in face_verts:
for loop in face.loops:
if loop.vert is vert:
face_loops.append(loop[uv_layer])
break
return [face_verts, face_edges, face_loops]

View File

@ -0,0 +1,117 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
import bmesh
from bpy.props import BoolProperty, EnumProperty, FloatProperty
from . import muv_common
class MUV_UnwrapConstraint(bpy.types.Operator):
"""
Operation class: Unwrap with constrain UV coordinate
"""
bl_idname = "uv.muv_unwrap_constraint"
bl_label = "Unwrap Constraint"
bl_description = "Unwrap while keeping uv coordinate"
bl_options = {'REGISTER', 'UNDO'}
# property for original unwrap
method = EnumProperty(
name="Method",
description="Unwrapping method",
items=[
('ANGLE_BASED', 'Angle Based', 'Angle Based'),
('CONFORMAL', 'Conformal', 'Conformal')
],
default='ANGLE_BASED')
fill_holes = BoolProperty(
name="Fill Holes",
description="Virtual fill holes in meshes before unwrapping",
default=True)
correct_aspect = BoolProperty(
name="Correct Aspect",
description="Map UVs taking image aspect ratio into account",
default=True)
use_subsurf_data = BoolProperty(
name="Use Subsurf Modifier",
description="""Map UVs taking vertex position after subsurf
into account""",
default=False)
margin = FloatProperty(
name="Margin",
description="Space between islands",
max=1.0,
min=0.0,
default=0.001)
# property for this operation
u_const = BoolProperty(
name="U-Constraint",
description="Keep UV U-axis coordinate",
default=False)
v_const = BoolProperty(
name="V-Constraint",
description="Keep UV V-axis coordinate",
default=False)
def execute(self, _):
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
# get original UV coordinate
faces = [f for f in bm.faces if f.select]
uv_list = []
for f in faces:
uvs = [l[uv_layer].uv.copy() for l in f.loops]
uv_list.append(uvs)
# unwrap
bpy.ops.uv.unwrap(
method=self.method,
fill_holes=self.fill_holes,
correct_aspect=self.correct_aspect,
use_subsurf_data=self.use_subsurf_data,
margin=self.margin)
# when U/V-Constraint is checked, revert original coordinate
for f, uvs in zip(faces, uv_list):
for l, uv in zip(f.loops, uvs):
if self.u_const:
l[uv_layer].uv.x = uv.x
if self.v_const:
l[uv_layer].uv.y = uv.y
# update mesh
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}

755
uv_magic_uv/muv_uvbb_ops.py Normal file
View File

@ -0,0 +1,755 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
from enum import IntEnum
import math
import bpy
import bgl
import mathutils
import bmesh
from . import muv_common
MAX_VALUE = 100000.0
class MUV_UVBBCmd():
"""
Custom class: Base class of command
"""
def __init__(self):
self.op = 'NONE' # operation
def to_matrix(self):
# mat = I
mat = mathutils.Matrix()
mat.identity()
return mat
class MUV_UVBBTranslationCmd(MUV_UVBBCmd):
"""
Custom class: Translation operation
"""
def __init__(self, ix, iy):
super().__init__()
self.op = 'TRANSLATION'
self.__x = ix # current x
self.__y = iy # current y
self.__ix = ix # initial x
self.__iy = iy # initial y
def to_matrix(self):
# mat = Mt
dx = self.__x - self.__ix
dy = self.__y - self.__iy
return mathutils.Matrix.Translation((dx, dy, 0))
def set(self, x, y):
self.__x = x
self.__y = y
class MUV_UVBBRotationCmd(MUV_UVBBCmd):
"""
Custom class: Rotation operation
"""
def __init__(self, ix, iy, cx, cy):
super().__init__()
self.op = 'ROTATION'
self.__x = ix # current x
self.__y = iy # current y
self.__cx = cx # center of rotation x
self.__cy = cy # center of rotation y
dx = self.__x - self.__cx
dy = self.__y - self.__cy
self.__iangle = math.atan2(dy, dx) # initial rotation angle
def to_matrix(self):
# mat = Mt * Mr * Mt^-1
dx = self.__x - self.__cx
dy = self.__y - self.__cy
angle = math.atan2(dy, dx) - self.__iangle
mti = mathutils.Matrix.Translation((-self.__cx, -self.__cy, 0.0))
mr = mathutils.Matrix.Rotation(angle, 4, 'Z')
mt = mathutils.Matrix.Translation((self.__cx, self.__cy, 0.0))
return mt * mr * mti
def set(self, x, y):
self.__x = x
self.__y = y
class MUV_UVBBScalingCmd(MUV_UVBBCmd):
"""
Custom class: Scaling operation
"""
def __init__(self, ix, iy, ox, oy, dir_x, dir_y, mat):
super().__init__()
self.op = 'SCALING'
self.__ix = ix # initial x
self.__iy = iy # initial y
self.__x = ix # current x
self.__y = iy # current y
self.__ox = ox # origin of scaling x
self.__oy = oy # origin of scaling y
self.__dir_x = dir_x # direction of scaling x
self.__dir_y = dir_y # direction of scaling y
self.__mat = mat
# initial origin of scaling = M(to original transform) * (ox, oy)
iov = mat * mathutils.Vector((ox, oy, 0.0))
self.__iox = iov.x # initial origin of scaling X
self.__ioy = iov.y # initial origin of scaling y
def to_matrix(self):
"""
mat = M(to original transform)^-1 * Mt(to origin) * Ms *
Mt(to origin)^-1 * M(to original transform)
"""
m = self.__mat
mi = self.__mat.inverted()
mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0))
mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0))
# every point must be transformed to origin
t = m * mathutils.Vector((self.__ix, self.__iy, 0.0))
tix, tiy = t.x, t.y
t = m * mathutils.Vector((self.__ox, self.__oy, 0.0))
tox, toy = t.x, t.y
t = m * mathutils.Vector((self.__x, self.__y, 0.0))
tx, ty = t.x, t.y
ms = mathutils.Matrix()
ms.identity()
if self.__dir_x == 1:
ms[0][0] = (tx - tox) * self.__dir_x / (tix - tox)
if self.__dir_y == 1:
ms[1][1] = (ty - toy) * self.__dir_y / (tiy - toy)
return mi * mto * ms * mtoi * m
def set(self, x, y):
self.__x = x
self.__y = y
class MUV_UVBBUniformScalingCmd(MUV_UVBBCmd):
"""
Custom class: Uniform Scaling operation
"""
def __init__(self, ix, iy, ox, oy, mat):
super().__init__()
self.op = 'SCALING'
self.__ix = ix # initial x
self.__iy = iy # initial y
self.__x = ix # current x
self.__y = iy # current y
self.__ox = ox # origin of scaling x
self.__oy = oy # origin of scaling y
self.__mat = mat
# initial origin of scaling = M(to original transform) * (ox, oy)
iov = mat * mathutils.Vector((ox, oy, 0.0))
self.__iox = iov.x # initial origin of scaling x
self.__ioy = iov.y # initial origin of scaling y
self.__dir_x = 1
self.__dir_y = 1
def to_matrix(self):
"""
mat = M(to original transform)^-1 * Mt(to origin) * Ms *
Mt(to origin)^-1 * M(to original transform)
"""
m = self.__mat
mi = self.__mat.inverted()
mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0))
mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0))
# every point must be transformed to origin
t = m * mathutils.Vector((self.__ix, self.__iy, 0.0))
tix, tiy = t.x, t.y
t = m * mathutils.Vector((self.__ox, self.__oy, 0.0))
tox, toy = t.x, t.y
t = m * mathutils.Vector((self.__x, self.__y, 0.0))
tx, ty = t.x, t.y
ms = mathutils.Matrix()
ms.identity()
tir = math.sqrt((tix - tox) * (tix - tox) + (tiy - toy) * (tiy - toy))
tr = math.sqrt((tx - tox) * (tx - tox) + (ty - toy) * (ty - toy))
sr = tr / tir
if ((tx - tox) * (tix - tox)) > 0:
self.__dir_x = 1
else:
self.__dir_x = -1
if ((ty - toy) * (tiy - toy)) > 0:
self.__dir_y = 1
else:
self.__dir_y = -1
ms[0][0] = sr * self.__dir_x
ms[1][1] = sr * self.__dir_y
return mi * mto * ms * mtoi * m
def set(self, x, y):
self.__x = x
self.__y = y
class MUV_UVBBCmdExecuter():
"""
Custom class: manage command history and execute command
"""
def __init__(self):
self.__cmd_list = [] # history
self.__cmd_list_redo = [] # redo list
def execute(self, begin=0, end=-1):
"""
create matrix from history
"""
mat = mathutils.Matrix()
mat.identity()
for i, cmd in enumerate(self.__cmd_list):
if begin <= i and (end == -1 or i <= end):
mat = cmd.to_matrix() * mat
return mat
def undo_size(self):
"""
get history size
"""
return len(self.__cmd_list)
def top(self):
"""
get top of history
"""
if len(self.__cmd_list) <= 0:
return None
return self.__cmd_list[-1]
def append(self, cmd):
"""
append command
"""
self.__cmd_list.append(cmd)
self.__cmd_list_redo = []
def undo(self):
"""
undo command
"""
if len(self.__cmd_list) <= 0:
return
self.__cmd_list_redo.append(self.__cmd_list.pop())
def redo(self):
"""
redo command
"""
if len(self.__cmd_list_redo) <= 0:
return
self.__cmd_list.append(self.__cmd_list_redo.pop())
def pop(self):
if len(self.__cmd_list) <= 0:
return None
return self.__cmd_list.pop()
def push(self, cmd):
self.__cmd_list.append(cmd)
class MUV_UVBBRenderer(bpy.types.Operator):
"""
Operation class: Render UV bounding box
"""
bl_idname = "uv.muv_uvbb_renderer"
bl_label = "UV Bounding Box Renderer"
bl_description = "Bounding Box Renderer about UV in Image Editor"
__handle = None
@staticmethod
def handle_add(obj, context):
if MUV_UVBBRenderer.__handle is None:
sie = bpy.types.SpaceImageEditor
MUV_UVBBRenderer.__handle = sie.draw_handler_add(
MUV_UVBBRenderer.draw_bb,
(obj, context), "WINDOW", "POST_PIXEL")
@staticmethod
def handle_remove():
if MUV_UVBBRenderer.__handle is not None:
sie = bpy.types.SpaceImageEditor
sie.draw_handler_remove(
MUV_UVBBRenderer.__handle, "WINDOW")
MUV_UVBBRenderer.__handle = None
@staticmethod
def __draw_ctrl_point(context, pos):
"""
Draw control point
"""
prefs = context.user_preferences.addons["uv_magic_uv"].preferences
cp_size = prefs.uvbb_cp_size
offset = cp_size / 2
verts = [
[pos.x - offset, pos.y - offset],
[pos.x - offset, pos.y + offset],
[pos.x + offset, pos.y + offset],
[pos.x + offset, pos.y - offset]
]
bgl.glEnable(bgl.GL_BLEND)
bgl.glBegin(bgl.GL_QUADS)
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
for (x, y) in verts:
bgl.glVertex2f(x, y)
bgl.glEnd()
@staticmethod
def draw_bb(_, context):
"""
Draw bounding box
"""
props = context.scene.muv_props.uvbb
for cp in props.ctrl_points:
MUV_UVBBRenderer.__draw_ctrl_point(
context, mathutils.Vector(
context.region.view2d.view_to_region(cp.x, cp.y)))
class MUV_UVBBState(IntEnum):
"""
Enum: State definition used by MUV_UVBBStateMgr
"""
NONE = 0
TRANSLATING = 1
SCALING_1 = 2
SCALING_2 = 3
SCALING_3 = 4
SCALING_4 = 5
SCALING_5 = 6
SCALING_6 = 7
SCALING_7 = 8
SCALING_8 = 9
ROTATING = 10
UNIFORM_SCALING_1 = 11
UNIFORM_SCALING_2 = 12
UNIFORM_SCALING_3 = 13
UNIFORM_SCALING_4 = 14
class MUV_UVBBStateBase():
"""
Custom class: Base class of state
"""
def __init__(self):
pass
def update(self, context, event, ctrl_points, mouse_view):
raise NotImplementedError
class MUV_UVBBStateNone(MUV_UVBBStateBase):
"""
Custom class:
No state
Wait for event from mouse
"""
def __init__(self, cmd_exec):
super().__init__()
self.__cmd_exec = cmd_exec
def update(self, context, event, ctrl_points, mouse_view):
"""
Update state
"""
prefs = context.user_preferences.addons["uv_magic_uv"].preferences
cp_react_size = prefs.uvbb_cp_react_size
is_uscaling = context.scene.muv_uvbb_uniform_scaling
if event.type == 'LEFTMOUSE':
if event.value == 'PRESS':
x, y = context.region.view2d.view_to_region(
mouse_view.x, mouse_view.y)
for i, p in enumerate(ctrl_points):
px, py = context.region.view2d.view_to_region(p.x, p.y)
in_cp_x = (px + cp_react_size > x and
px - cp_react_size < x)
in_cp_y = (py + cp_react_size > y and
py - cp_react_size < y)
if in_cp_x and in_cp_y:
if is_uscaling:
arr = [1, 3, 6, 8]
if i in arr:
return (
MUV_UVBBState.UNIFORM_SCALING_1
+ arr.index(i)
)
else:
return MUV_UVBBState.TRANSLATING + i
return MUV_UVBBState.NONE
class MUV_UVBBStateTranslating(MUV_UVBBStateBase):
"""
Custom class: Translating state
"""
def __init__(self, cmd_exec, ctrl_points):
super().__init__()
self.__cmd_exec = cmd_exec
ix, iy = ctrl_points[0].x, ctrl_points[0].y
self.__cmd_exec.append(MUV_UVBBTranslationCmd(ix, iy))
def update(self, context, event, ctrl_points, mouse_view):
if event.type == 'LEFTMOUSE':
if event.value == 'RELEASE':
return MUV_UVBBState.NONE
if event.type == 'MOUSEMOVE':
x, y = mouse_view.x, mouse_view.y
self.__cmd_exec.top().set(x, y)
return MUV_UVBBState.TRANSLATING
class MUV_UVBBStateScaling(MUV_UVBBStateBase):
"""
Custom class: Scaling state
"""
def __init__(self, cmd_exec, state, ctrl_points):
super().__init__()
self.__state = state
self.__cmd_exec = cmd_exec
dir_x_list = [1, 1, 1, 0, 0, 1, 1, 1]
dir_y_list = [1, 0, 1, 1, 1, 1, 0, 1]
idx = state - 2
ix, iy = ctrl_points[idx + 1].x, ctrl_points[idx + 1].y
ox, oy = ctrl_points[8 - idx].x, ctrl_points[8 - idx].y
dir_x, dir_y = dir_x_list[idx], dir_y_list[idx]
mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size())
self.__cmd_exec.append(
MUV_UVBBScalingCmd(ix, iy, ox, oy, dir_x, dir_y, mat.inverted()))
def update(self, context, event, ctrl_points, mouse_view):
if event.type == 'LEFTMOUSE':
if event.value == 'RELEASE':
return MUV_UVBBState.NONE
if event.type == 'MOUSEMOVE':
x, y = mouse_view.x, mouse_view.y
self.__cmd_exec.top().set(x, y)
return self.__state
class MUV_UVBBStateUniformScaling(MUV_UVBBStateBase):
"""
Custom class: Uniform Scaling state
"""
def __init__(self, cmd_exec, state, ctrl_points):
super().__init__()
self.__state = state
self.__cmd_exec = cmd_exec
icp_idx = [1, 3, 6, 8]
ocp_idx = [8, 6, 3, 1]
idx = state - MUV_UVBBState.UNIFORM_SCALING_1
ix, iy = ctrl_points[icp_idx[idx]].x, ctrl_points[icp_idx[idx]].y
ox, oy = ctrl_points[ocp_idx[idx]].x, ctrl_points[ocp_idx[idx]].y
mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size())
self.__cmd_exec.append(MUV_UVBBUniformScalingCmd(
ix, iy, ox, oy, mat.inverted()))
def update(self, context, event, ctrl_points, mouse_view):
if event.type == 'LEFTMOUSE':
if event.value == 'RELEASE':
return MUV_UVBBState.NONE
if event.type == 'MOUSEMOVE':
x, y = mouse_view.x, mouse_view.y
self.__cmd_exec.top().set(x, y)
return self.__state
class MUV_UVBBStateRotating(MUV_UVBBStateBase):
"""
Custom class: Rotating state
"""
def __init__(self, cmd_exec, ctrl_points):
super().__init__()
self.__cmd_exec = cmd_exec
ix, iy = ctrl_points[9].x, ctrl_points[9].y
ox, oy = ctrl_points[0].x, ctrl_points[0].y
self.__cmd_exec.append(MUV_UVBBRotationCmd(ix, iy, ox, oy))
def update(self, context, event, ctrl_points, mouse_view):
if event.type == 'LEFTMOUSE':
if event.value == 'RELEASE':
return MUV_UVBBState.NONE
if event.type == 'MOUSEMOVE':
x, y = mouse_view.x, mouse_view.y
self.__cmd_exec.top().set(x, y)
return MUV_UVBBState.ROTATING
class MUV_UVBBStateMgr():
"""
Custom class: Manage state about this feature
"""
def __init__(self, cmd_exec):
self.__cmd_exec = cmd_exec # command executer
self.__state = MUV_UVBBState.NONE # current state
self.__state_obj = MUV_UVBBStateNone(self.__cmd_exec)
def __update_state(self, next_state, ctrl_points):
"""
Update state
"""
if next_state == self.__state:
return
obj = None
if next_state == MUV_UVBBState.TRANSLATING:
obj = MUV_UVBBStateTranslating(self.__cmd_exec, ctrl_points)
elif MUV_UVBBState.SCALING_1 <= next_state <= MUV_UVBBState.SCALING_8:
obj = MUV_UVBBStateScaling(
self.__cmd_exec, next_state, ctrl_points)
elif next_state == MUV_UVBBState.ROTATING:
obj = MUV_UVBBStateRotating(self.__cmd_exec, ctrl_points)
elif next_state == MUV_UVBBState.NONE:
obj = MUV_UVBBStateNone(self.__cmd_exec)
elif (MUV_UVBBState.UNIFORM_SCALING_1 <= next_state
<= MUV_UVBBState.UNIFORM_SCALING_4):
obj = MUV_UVBBStateUniformScaling(
self.__cmd_exec, next_state, ctrl_points)
if obj is not None:
self.__state_obj = obj
self.__state = next_state
def update(self, context, ctrl_points, event):
mouse_region = mathutils.Vector((
event.mouse_region_x, event.mouse_region_y))
mouse_view = mathutils.Vector((context.region.view2d.region_to_view(
mouse_region.x, mouse_region.y)))
next_state = self.__state_obj.update(
context, event, ctrl_points, mouse_view)
self.__update_state(next_state, ctrl_points)
class MUV_UVBBUpdater(bpy.types.Operator):
"""
Operation class: Update state and handle event by modal function
"""
bl_idname = "uv.muv_uvbb_updater"
bl_label = "UV Bounding Box Updater"
bl_description = "Update UV Bounding Box"
bl_options = {'REGISTER', 'UNDO'}
def __init__(self):
self.__timer = None
self.__cmd_exec = MUV_UVBBCmdExecuter() # Command executer
self.__state_mgr = MUV_UVBBStateMgr(self.__cmd_exec) # State Manager
def __handle_add(self, context):
if self.__timer is None:
self.__timer = context.window_manager.event_timer_add(
0.1, context.window)
context.window_manager.modal_handler_add(self)
MUV_UVBBRenderer.handle_add(self, context)
def __handle_remove(self, context):
MUV_UVBBRenderer.handle_remove()
if self.__timer is not None:
context.window_manager.event_timer_remove(self.__timer)
self.__timer = None
def __get_uv_info(self, context):
"""
Get UV coordinate
"""
obj = context.active_object
uv_info = []
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
return None
uv_layer = bm.loops.layers.uv.verify()
for f in bm.faces:
if f.select:
for i, l in enumerate(f.loops):
uv_info.append((f.index, i, l[uv_layer].uv.copy()))
if len(uv_info) == 0:
return None
return uv_info
def __get_ctrl_point(self, uv_info_ini):
"""
Get control point
"""
left = MAX_VALUE
right = -MAX_VALUE
top = -MAX_VALUE
bottom = MAX_VALUE
for info in uv_info_ini:
uv = info[2]
if uv.x < left:
left = uv.x
if uv.x > right:
right = uv.x
if uv.y < bottom:
bottom = uv.y
if uv.y > top:
top = uv.y
points = [
mathutils.Vector((
(left + right) * 0.5, (top + bottom) * 0.5, 0.0
)),
mathutils.Vector((left, top, 0.0)),
mathutils.Vector((left, (top + bottom) * 0.5, 0.0)),
mathutils.Vector((left, bottom, 0.0)),
mathutils.Vector(((left + right) * 0.5, top, 0.0)),
mathutils.Vector(((left + right) * 0.5, bottom, 0.0)),
mathutils.Vector((right, top, 0.0)),
mathutils.Vector((right, (top + bottom) * 0.5, 0.0)),
mathutils.Vector((right, bottom, 0.0)),
mathutils.Vector(((left + right) * 0.5, top + 0.03, 0.0))
]
return points
def __update_uvs(self, context, uv_info_ini, trans_mat):
"""
Update UV coordinate
"""
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
return
uv_layer = bm.loops.layers.uv.verify()
for info in uv_info_ini:
fidx = info[0]
lidx = info[1]
uv = info[2]
v = mathutils.Vector((uv.x, uv.y, 0.0))
av = trans_mat * v
bm.faces[fidx].loops[lidx][uv_layer].uv = mathutils.Vector(
(av.x, av.y))
def __update_ctrl_point(self, ctrl_points_ini, trans_mat):
"""
Update control point
"""
return [trans_mat * cp for cp in ctrl_points_ini]
def modal(self, context, event):
props = context.scene.muv_props.uvbb
muv_common.redraw_all_areas()
if props.running is False:
self.__handle_remove(context)
return {'FINISHED'}
if event.type == 'TIMER':
trans_mat = self.__cmd_exec.execute()
self.__update_uvs(context, props.uv_info_ini, trans_mat)
props.ctrl_points = self.__update_ctrl_point(
props.ctrl_points_ini, trans_mat)
self.__state_mgr.update(context, props.ctrl_points, event)
return {'PASS_THROUGH'}
def execute(self, context):
props = context.scene.muv_props.uvbb
if props.running is True:
props.running = False
return {'FINISHED'}
props.uv_info_ini = self.__get_uv_info(context)
if props.uv_info_ini is None:
return {'CANCELLED'}
props.ctrl_points_ini = self.__get_ctrl_point(props.uv_info_ini)
trans_mat = self.__cmd_exec.execute()
# Update is needed in order to display control point
self.__update_uvs(context, props.uv_info_ini, trans_mat)
props.ctrl_points = self.__update_ctrl_point(
props.ctrl_points_ini, trans_mat)
self.__handle_add(context)
props.running = True
return {'RUNNING_MODAL'}
class IMAGE_PT_MUV_UVBB(bpy.types.Panel):
"""
Panel class: UV Bounding Box Menu on Property Panel on UV/ImageEditor
"""
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_label = "UV Bounding Box"
bl_context = 'mesh_edit'
@classmethod
def poll(cls, context):
prefs = context.user_preferences.addons["uv_magic_uv"].preferences
return prefs.enable_uvbb
def draw_header(self, _):
layout = self.layout
layout.label(text="", icon='PLUGIN')
def draw(self, context):
sc = context.scene
props = sc.muv_props.uvbb
layout = self.layout
if props.running is False:
layout.operator(
MUV_UVBBUpdater.bl_idname, text="Display UV Bounding Box",
icon='PLAY')
else:
layout.operator(
MUV_UVBBUpdater.bl_idname, text="Hide UV Bounding Box",
icon='PAUSE')
layout.prop(sc, "muv_uvbb_uniform_scaling", text="Uniform Scaling")

151
uv_magic_uv/muv_wsuv_ops.py Normal file
View File

@ -0,0 +1,151 @@
# <pep8-80 compliant>
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
__author__ = "McBuff, Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "4.3"
__date__ = "1 Apr 2017"
import bpy
import bmesh
from . import muv_common
def calc_edge_scale(uv_layer, loop0, loop1):
v0 = loop0.vert.co
v1 = loop1.vert.co
uv0 = loop0[uv_layer].uv.copy()
uv1 = loop1[uv_layer].uv.copy()
dv = v1 - v0
duv = uv1 - uv0
scale = 0.0
if dv.magnitude > 0.00000001:
scale = duv.magnitude / dv.magnitude
return scale
def calc_face_scale(uv_layer, face):
es = 0.0
for i, l in enumerate(face.loops[1:]):
es = es + calc_edge_scale(uv_layer, face.loops[i], l)
return es
class MUV_WSUVMeasure(bpy.types.Operator):
"""
Operation class: Measure face size
"""
bl_idname = "uv.muv_wsuv_measure"
bl_label = "Measure"
bl_description = "Measure face size for scale calculation"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.muv_props.wsuv
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report({'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
sel_faces = [f for f in bm.faces if f.select]
# measure average face size
scale = 0.0
for f in sel_faces:
scale = scale + calc_face_scale(uv_layer, f)
props.ref_scale = scale / len(sel_faces)
return {'FINISHED'}
class MUV_WSUVApply(bpy.types.Operator):
"""
Operation class: Apply scaled UV
"""
bl_idname = "uv.muv_wsuv_apply"
bl_label = "Apply"
bl_description = "Apply scaled UV based on scale calculation"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = context.scene.muv_props.wsuv
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if muv_common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report(
{'WARNING'}, "Object must have more than one UV map")
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
sel_faces = [f for f in bm.faces if f.select]
# measure average face size
scale = 0.0
for f in sel_faces:
scale = scale + calc_face_scale(uv_layer, f)
scale = scale / len(sel_faces)
ratio = props.ref_scale / scale
orig_area = bpy.context.area.type
bpy.context.area.type = 'IMAGE_EDITOR'
# apply scaled UV
bpy.ops.transform.resize(
value=(ratio, ratio, ratio),
constraint_axis=(False, False, False),
constraint_orientation='GLOBAL',
mirror=False,
proportional='DISABLED',
proportional_edit_falloff='SMOOTH',
proportional_size=1,
snap=False,
snap_target='CLOSEST',
snap_point=(0, 0, 0),
snap_align=False,
snap_normal=(0, 0, 0),
texture_space=False,
release_confirm=False)
bpy.context.area.type = orig_area
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}