Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCampbell Barton <ideasman42@gmail.com>2018-12-19 03:56:05 +0300
committerCampbell Barton <ideasman42@gmail.com>2018-12-19 03:56:05 +0300
commit6a1ce20043860e4f836294d46a86b5fcf7fbf1a8 (patch)
treed99f11f29954067c147cde7b99e97ddb19ef5e00
parent156c5ea6a45d0549d8422e3ea295972dec7766c3 (diff)
parent9cc2ad1eaf941d8ed3b5542a3d5cdfccec7ba60b (diff)
Merge branch 'master' into blender2.8
-rw-r--r--ant_landscape/__init__.py4
-rw-r--r--ant_landscape/ant_functions.py4
-rw-r--r--archipack/archipack_fence.py2
-rw-r--r--archipack/archipack_stair.py2
-rw-r--r--development_iskeyfree.py10
-rw-r--r--io_import_images_as_planes.py2
-rw-r--r--io_scene_fbx/import_fbx.py2
-rw-r--r--measureit/measureit_geometry.py6
-rw-r--r--mesh_bsurfaces.py2
-rw-r--r--mesh_extra_tools/mesh_extrude_and_reshape.py2
-rw-r--r--node_wrangler.py7
-rw-r--r--presets/operator/mesh.landscape_add/canyon.py (renamed from presets/operator/mesh.landscape_add/canion.py)0
-rw-r--r--presets/operator/mesh.landscape_add/canyons.py (renamed from presets/operator/mesh.landscape_add/canions.py)0
-rw-r--r--presets/operator/mesh.landscape_add/crystalline.py (renamed from presets/operator/mesh.landscape_add/cristaline.py)0
-rw-r--r--presets/operator/mesh.landscape_add/volcano.py (renamed from presets/operator/mesh.landscape_add/vulcano.py)0
-rw-r--r--render_freestyle_svg.py2
-rw-r--r--space_view3d_pie_menus/pie_save_open_menu.py2
-rw-r--r--uv_magic_uv/__init__.py81
-rw-r--r--uv_magic_uv/addon_updater.py1501
-rw-r--r--uv_magic_uv/addon_updater_ops.py1357
-rw-r--r--uv_magic_uv/common.py534
-rw-r--r--uv_magic_uv/impl/__init__.py0
-rw-r--r--uv_magic_uv/impl/copy_paste_uv_impl.py271
-rw-r--r--uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py166
-rw-r--r--uv_magic_uv/impl/flip_rotate_impl.py133
-rw-r--r--uv_magic_uv/impl/mirror_uv_impl.py158
-rw-r--r--uv_magic_uv/impl/move_uv_impl.py166
-rw-r--r--uv_magic_uv/impl/transfer_uv_impl.py330
-rw-r--r--uv_magic_uv/impl/uvw_impl.py154
-rw-r--r--uv_magic_uv/legacy/__init__.py38
-rw-r--r--uv_magic_uv/legacy/op/__init__.py74
-rw-r--r--uv_magic_uv/legacy/op/align_uv.py (renamed from uv_magic_uv/op/align_uv.py)399
-rw-r--r--uv_magic_uv/legacy/op/align_uv_cursor.py (renamed from uv_magic_uv/op/align_uv_cursor.py)117
-rw-r--r--uv_magic_uv/legacy/op/copy_paste_uv.py531
-rw-r--r--uv_magic_uv/legacy/op/copy_paste_uv_object.py298
-rw-r--r--uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py97
-rw-r--r--uv_magic_uv/legacy/op/flip_rotate_uv.py132
-rw-r--r--uv_magic_uv/legacy/op/mirror_uv.py110
-rw-r--r--uv_magic_uv/legacy/op/move_uv.py82
-rw-r--r--uv_magic_uv/legacy/op/pack_uv.py (renamed from uv_magic_uv/op/pack_uv.py)84
-rw-r--r--uv_magic_uv/legacy/op/preserve_uv_aspect.py (renamed from uv_magic_uv/op/preserve_uv_aspect.py)94
-rw-r--r--uv_magic_uv/legacy/op/select_uv.py168
-rw-r--r--uv_magic_uv/legacy/op/smooth_uv.py (renamed from uv_magic_uv/op/smooth_uv.py)84
-rw-r--r--uv_magic_uv/legacy/op/texture_lock.py (renamed from uv_magic_uv/op/texture_lock.py)319
-rw-r--r--uv_magic_uv/legacy/op/texture_projection.py402
-rw-r--r--uv_magic_uv/legacy/op/texture_wrap.py (renamed from uv_magic_uv/op/texture_wrap.py)117
-rw-r--r--uv_magic_uv/legacy/op/transfer_uv.py172
-rw-r--r--uv_magic_uv/legacy/op/unwrap_constraint.py (renamed from uv_magic_uv/op/unwrap_constraint.py)76
-rw-r--r--uv_magic_uv/legacy/op/uv_bounding_box.py (renamed from uv_magic_uv/op/uv_bounding_box.py)382
-rw-r--r--uv_magic_uv/legacy/op/uv_inspection.py280
-rw-r--r--uv_magic_uv/legacy/op/uv_sculpt.py (renamed from uv_magic_uv/op/uv_sculpt.py)285
-rw-r--r--uv_magic_uv/legacy/op/uvw.py181
-rw-r--r--uv_magic_uv/legacy/op/world_scale_uv.py655
-rw-r--r--uv_magic_uv/legacy/preferences.py468
-rw-r--r--uv_magic_uv/legacy/properites.py61
-rw-r--r--uv_magic_uv/legacy/ui/IMAGE_MT_uvs.py197
-rw-r--r--uv_magic_uv/legacy/ui/VIEW3D_MT_object.py54
-rw-r--r--uv_magic_uv/legacy/ui/VIEW3D_MT_uv_map.py257
-rw-r--r--uv_magic_uv/legacy/ui/__init__.py50
-rw-r--r--uv_magic_uv/legacy/ui/uvedit_copy_paste_uv.py62
-rw-r--r--uv_magic_uv/legacy/ui/uvedit_editor_enhancement.py149
-rw-r--r--uv_magic_uv/legacy/ui/uvedit_uv_manipulation.py130
-rw-r--r--uv_magic_uv/legacy/ui/view3d_copy_paste_uv_editmode.py93
-rw-r--r--uv_magic_uv/legacy/ui/view3d_copy_paste_uv_objectmode.py65
-rw-r--r--uv_magic_uv/legacy/ui/view3d_uv_manipulation.py289
-rw-r--r--uv_magic_uv/legacy/ui/view3d_uv_mapping.py116
-rw-r--r--uv_magic_uv/op/__init__.py30
-rw-r--r--uv_magic_uv/op/copy_paste_uv.py744
-rw-r--r--uv_magic_uv/op/copy_paste_uv_object.py314
-rw-r--r--uv_magic_uv/op/copy_paste_uv_uvedit.py145
-rw-r--r--uv_magic_uv/op/flip_rotate_uv.py112
-rw-r--r--uv_magic_uv/op/mirror_uv.py143
-rw-r--r--uv_magic_uv/op/move_uv.py137
-rw-r--r--uv_magic_uv/op/texture_projection.py296
-rw-r--r--uv_magic_uv/op/transfer_uv.py378
-rw-r--r--uv_magic_uv/op/uv_inspection.py623
-rw-r--r--uv_magic_uv/op/uvw.py196
-rw-r--r--uv_magic_uv/op/world_scale_uv.py236
-rw-r--r--uv_magic_uv/preferences.py459
-rw-r--r--uv_magic_uv/properites.py745
-rw-r--r--uv_magic_uv/ui/IMAGE_MT_uvs.py56
-rw-r--r--uv_magic_uv/ui/VIEW3D_MT_object.py54
-rw-r--r--uv_magic_uv/ui/VIEW3D_MT_uv_map.py111
-rw-r--r--uv_magic_uv/ui/__init__.py18
-rw-r--r--uv_magic_uv/ui/uvedit_copy_paste_uv.py26
-rw-r--r--uv_magic_uv/ui/uvedit_editor_enhance.py136
-rw-r--r--uv_magic_uv/ui/uvedit_uv_manipulation.py117
-rw-r--r--uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py68
-rw-r--r--uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py29
-rw-r--r--uv_magic_uv/ui/view3d_uv_manipulation.py160
-rw-r--r--uv_magic_uv/ui/view3d_uv_mapping.py63
-rw-r--r--uv_magic_uv/utils/__init__.py34
-rw-r--r--uv_magic_uv/utils/bl_class_registry.py84
-rw-r--r--uv_magic_uv/utils/property_class_registry.py72
94 files changed, 13315 insertions, 4337 deletions
diff --git a/ant_landscape/__init__.py b/ant_landscape/__init__.py
index fd758e33..49432e81 100644
--- a/ant_landscape/__init__.py
+++ b/ant_landscape/__init__.py
@@ -391,7 +391,7 @@ class AntDisplaceSettingsPanel(bpy.types.Panel):
if not ant.sphere_mesh:
col = box.column()
col.prop(ant, "edge_falloff")
- if ant.edge_falloff is not "0":
+ if ant.edge_falloff != "0":
col = box.column(align=True)
col.prop(ant, "edge_level")
if ant.edge_falloff in ["2", "3"]:
@@ -401,7 +401,7 @@ class AntDisplaceSettingsPanel(bpy.types.Panel):
col = box.column()
col.prop(ant, "strata_type")
- if ant.strata_type is not "0":
+ if ant.strata_type != "0":
col = box.column()
col.prop(ant, "strata")
col = box.column()
diff --git a/ant_landscape/ant_functions.py b/ant_landscape/ant_functions.py
index bdabb62c..ca81ce6d 100644
--- a/ant_landscape/ant_functions.py
+++ b/ant_landscape/ant_functions.py
@@ -662,7 +662,7 @@ def draw_ant_displace(self, context, generate=True):
if not self.sphere_mesh:
col = box.column()
col.prop(self, "edge_falloff")
- if self.edge_falloff is not "0":
+ if self.edge_falloff != "0":
col = box.column(align=True)
col.prop(self, "edge_level")
if self.edge_falloff in ["2", "3"]:
@@ -672,7 +672,7 @@ def draw_ant_displace(self, context, generate=True):
col = box.column()
col.prop(self, "strata_type")
- if self.strata_type is not "0":
+ if self.strata_type != "0":
col = box.column()
col.prop(self, "strata")
diff --git a/archipack/archipack_fence.py b/archipack/archipack_fence.py
index 376f90e6..5f987779 100644
--- a/archipack/archipack_fence.py
+++ b/archipack/archipack_fence.py
@@ -1482,7 +1482,7 @@ class ARCHIPACK_PT_fence(Panel):
row = box.row(align=True)
row.operator("archipack.fence_curve_update", text="", icon='FILE_REFRESH')
row.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE')
- if prop.user_defined_path is not "":
+ if prop.user_defined_path != "":
box.prop(prop, 'user_defined_spline')
box.prop(prop, 'user_defined_resolution')
box.prop(prop, 'angle_limit')
diff --git a/archipack/archipack_stair.py b/archipack/archipack_stair.py
index 578208f5..c0d75317 100644
--- a/archipack/archipack_stair.py
+++ b/archipack/archipack_stair.py
@@ -2518,7 +2518,7 @@ class archipack_stair(ArchipackObject, Manipulable, PropertyGroup):
self.setup_manipulators()
- if self.presets is not 'STAIR_O':
+ if self.presets != 'STAIR_O':
for i, part in enumerate(self.parts):
if i >= self.n_parts:
break
diff --git a/development_iskeyfree.py b/development_iskeyfree.py
index c2e3f34f..5d5600f4 100644
--- a/development_iskeyfree.py
+++ b/development_iskeyfree.py
@@ -105,18 +105,18 @@ class MyChecker():
cls.mylist.clear()
for e in sortkeys:
cmd = ""
- if e[2] is not "":
+ if e[2] != "":
cmd += e[2] + "+"
- if e[3] is not "":
+ if e[3] != "":
cmd += e[3] + "+"
- if e[4] is not "":
+ if e[4] != "":
cmd += e[4] + "+"
- if e[5] is not "":
+ if e[5] != "":
cmd += e[5] + "+"
cmd += e[1]
- if e[6] is not "":
+ if e[6] != "":
cmd += " " + e[6]
cls.mylist.append([e[0], cmd])
diff --git a/io_import_images_as_planes.py b/io_import_images_as_planes.py
index 2b57c676..88cee32a 100644
--- a/io_import_images_as_planes.py
+++ b/io_import_images_as_planes.py
@@ -1042,7 +1042,7 @@ class IMPORT_IMAGE_OT_to_plane(Operator, AddObjectHelper):
bpy.ops.mesh.primitive_plane_add('INVOKE_REGION_WIN')
plane = context.active_object
# Why does mesh.primitive_plane_add leave the object in edit mode???
- if plane.mode is not 'OBJECT':
+ if plane.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
plane.dimensions = width, height, 0.0
plane.data.name = plane.name = name
diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py
index 8e4747b0..f35a6387 100644
--- a/io_scene_fbx/import_fbx.py
+++ b/io_scene_fbx/import_fbx.py
@@ -1124,7 +1124,7 @@ def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None):
bdata = [None] * len(blen_data) if is_fake else blen_data
if func(mesh, bdata, "normal",
fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 3, 3, layer_id, xform, True):
- if blen_data_type is "Polygons":
+ if blen_data_type == "Polygons":
for pidx, p in enumerate(mesh.polygons):
for lidx in range(p.loop_start, p.loop_start + p.loop_total):
mesh.loops[lidx].normal[:] = bdata[pidx]
diff --git a/measureit/measureit_geometry.py b/measureit/measureit_geometry.py
index 191f981b..6340a890 100644
--- a/measureit/measureit_geometry.py
+++ b/measureit/measureit_geometry.py
@@ -835,9 +835,9 @@ def draw_text(myobj, pos2d, display_text, rgba, fsize, align='L', text_rot=0.0):
# -------------------
for line in mylines:
text_width, text_height = blf.dimensions(font_id, line)
- if align is 'C':
+ if align == 'C':
newx = x_pos - text_width / 2
- elif align is 'R':
+ elif align == 'R':
newx = x_pos - text_width - gap
else:
newx = x_pos
@@ -855,7 +855,7 @@ def draw_text(myobj, pos2d, display_text, rgba, fsize, align='L', text_rot=0.0):
if maxwidth < text_width:
maxwidth = text_width
- if align is 'L':
+ if align == 'L':
blf.disable(font_id, ROTATION)
return maxwidth, maxheight
diff --git a/mesh_bsurfaces.py b/mesh_bsurfaces.py
index ffe16e04..53b88d49 100644
--- a/mesh_bsurfaces.py
+++ b/mesh_bsurfaces.py
@@ -2928,7 +2928,7 @@ class GPENCIL_OT_SURFSK_add_surface(Operator):
for i in range(0, len(surface_splines_parsed[0])):
surface_splines_parsed[0][i] = self.main_object.matrix_world * verts_ordered_V2[i].co
- # When "Automatic join" option is active (and the selection type is not "TWO_CONNECTED"),
+ # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
# merge the verts of the tips of the loops when they are "near enough"
if self.automatic_join and selection_type != "TWO_CONNECTED":
# Join the tips of "Follow" loops that are near enough and must be "closed"
diff --git a/mesh_extra_tools/mesh_extrude_and_reshape.py b/mesh_extra_tools/mesh_extrude_and_reshape.py
index 696e775d..a7ca0fdd 100644
--- a/mesh_extra_tools/mesh_extrude_and_reshape.py
+++ b/mesh_extra_tools/mesh_extrude_and_reshape.py
@@ -245,7 +245,7 @@ class Extrude_and_Reshape(Operator):
@classmethod
def poll(cls, context):
- return context.mode is not 'EDIT_MESH'
+ return context.mode != 'EDIT_MESH'
def modal(self, context, event):
if self.confirm:
diff --git a/node_wrangler.py b/node_wrangler.py
index b27e469d..d3fd8448 100644
--- a/node_wrangler.py
+++ b/node_wrangler.py
@@ -2372,7 +2372,10 @@ class NWCopySettings(Operator, NWBase):
def poll(cls, context):
valid = False
if nw_check(context):
- if context.active_node is not None and context.active_node.type is not 'FRAME':
+ if (
+ context.active_node is not None and
+ context.active_node.type != 'FRAME'
+ ):
valid = True
return valid
@@ -2955,7 +2958,7 @@ class NWAddReroutes(Operator, NWBase):
reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
for out_i, output in enumerate(node.outputs):
pass_used = False # initial value to be analyzed if 'R_LAYERS'
- # if node is not 'R_LAYERS' - "pass_used" not needed, so set it to True
+ # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
if node.type != 'R_LAYERS':
pass_used = True
else: # if 'R_LAYERS' check if output represent used render pass
diff --git a/presets/operator/mesh.landscape_add/canion.py b/presets/operator/mesh.landscape_add/canyon.py
index 1d63470c..1d63470c 100644
--- a/presets/operator/mesh.landscape_add/canion.py
+++ b/presets/operator/mesh.landscape_add/canyon.py
diff --git a/presets/operator/mesh.landscape_add/canions.py b/presets/operator/mesh.landscape_add/canyons.py
index cc0402cb..cc0402cb 100644
--- a/presets/operator/mesh.landscape_add/canions.py
+++ b/presets/operator/mesh.landscape_add/canyons.py
diff --git a/presets/operator/mesh.landscape_add/cristaline.py b/presets/operator/mesh.landscape_add/crystalline.py
index 1155758b..1155758b 100644
--- a/presets/operator/mesh.landscape_add/cristaline.py
+++ b/presets/operator/mesh.landscape_add/crystalline.py
diff --git a/presets/operator/mesh.landscape_add/vulcano.py b/presets/operator/mesh.landscape_add/volcano.py
index e5c3de29..e5c3de29 100644
--- a/presets/operator/mesh.landscape_add/vulcano.py
+++ b/presets/operator/mesh.landscape_add/volcano.py
diff --git a/render_freestyle_svg.py b/render_freestyle_svg.py
index 6cc2e1b6..7e1ffc19 100644
--- a/render_freestyle_svg.py
+++ b/render_freestyle_svg.py
@@ -241,7 +241,7 @@ class SVGExport(bpy.types.PropertyGroup):
line_join_type = EnumProperty(
name="Linejoin",
items=(
- ('MITTER', "Mitter", "Corners are sharp", 0),
+ ('MITER', "Miter", "Corners are sharp", 0),
('ROUND', "Round", "Corners are smoothed", 1),
('BEVEL', "Bevel", "Corners are bevelled", 2),
),
diff --git a/space_view3d_pie_menus/pie_save_open_menu.py b/space_view3d_pie_menus/pie_save_open_menu.py
index a8307d07..e71f7924 100644
--- a/space_view3d_pie_menus/pie_save_open_menu.py
+++ b/space_view3d_pie_menus/pie_save_open_menu.py
@@ -130,7 +130,7 @@ class FileIncrementalSave(Operator):
@classmethod
def poll(cls, context):
- return (bpy.data.filepath is not "")
+ return (bpy.data.filepath != "")
def execute(self, context):
f_path = bpy.data.filepath
diff --git a/uv_magic_uv/__init__.py b/uv_magic_uv/__init__.py
index 080d2414..63591526 100644
--- a/uv_magic_uv/__init__.py
+++ b/uv_magic_uv/__init__.py
@@ -20,16 +20,16 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
bl_info = {
"name": "Magic UV",
"author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs"
"Keith (Wahooney) Boshoff, McBuff, MaxRobinot, Alexander Milovsky",
- "version": (5, 1, 0),
- "blender": (2, 79, 0),
+ "version": (5, 3, 0),
+ "blender": (2, 80, 0),
"location": "See Add-ons Preferences",
"description": "UV Toolset. See Add-ons Preferences for details",
"warning": "",
@@ -40,31 +40,80 @@ bl_info = {
"category": "UV"
}
+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
+ if bpy.app.version[1] > minor:
+ return 1
+ return -1
+
+
if "bpy" in locals():
import importlib
- importlib.reload(op)
- importlib.reload(ui)
importlib.reload(common)
- importlib.reload(preferences)
- importlib.reload(properites)
+ importlib.reload(utils)
+ utils.bl_class_registry.BlClassRegistry.cleanup()
+ if check_version(2, 80, 0) >= 0:
+ importlib.reload(op)
+ importlib.reload(ui)
+ importlib.reload(properites)
+ importlib.reload(preferences)
+ importlib.reload(addon_updater_ops)
+ importlib.reload(addon_updater)
+ else:
+ importlib.reload(legacy)
else:
- from . import op
- from . import ui
+ import bpy
from . import common
- from . import preferences
- from . import properites
+ from . import utils
+ if check_version(2, 80, 0) >= 0:
+ from . import op
+ from . import ui
+ from . import properites
+ from . import preferences
+ from . import addon_updater_ops
+ from . import addon_updater
+ else:
+ from . import legacy
+
import bpy
def register():
- bpy.utils.register_module(__name__)
- properites.init_props(bpy.types.Scene)
+ if common.check_version(2, 80, 0) >= 0:
+ utils.bl_class_registry.BlClassRegistry.register()
+ properites.init_props(bpy.types.Scene)
+ if preferences.Preferences.enable_builtin_menu:
+ preferences.add_builtin_menu()
+ else:
+ utils.bl_class_registry.BlClassRegistry.register()
+ legacy.properites.init_props(bpy.types.Scene)
+ if legacy.preferences.Preferences.enable_builtin_menu:
+ legacy.preferences.add_builtin_menu()
+ if not common.is_console_mode():
+ addon_updater_ops.register(bl_info)
def unregister():
- bpy.utils.unregister_module(__name__)
- properites.clear_props(bpy.types.Scene)
+ if common.check_version(2, 80, 0) >= 0:
+ if preferences.Preferences.enable_builtin_menu:
+ preferences.remove_builtin_menu()
+ properites.clear_props(bpy.types.Scene)
+ utils.bl_class_registry.BlClassRegistry.unregister()
+ else:
+ if not common.is_console_mode():
+ addon_updater_ops.unregister()
+ if legacy.preferences.Preferences.enable_builtin_menu:
+ legacy.preferences.remove_builtin_menu()
+ legacy.properites.clear_props(bpy.types.Scene)
+ utils.bl_class_registry.BlClassRegistry.unregister()
if __name__ == "__main__":
diff --git a/uv_magic_uv/addon_updater.py b/uv_magic_uv/addon_updater.py
new file mode 100644
index 00000000..70b6a287
--- /dev/null
+++ b/uv_magic_uv/addon_updater.py
@@ -0,0 +1,1501 @@
+# ##### 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 #####
+
+
+"""
+See documentation for usage
+https://github.com/CGCookie/blender-addon-updater
+
+"""
+
+import ssl
+import urllib.request
+import urllib
+import os
+import json
+import zipfile
+import shutil
+import asyncio
+import threading
+import time
+import fnmatch
+from datetime import datetime, timedelta
+
+# blender imports, used in limited cases
+import bpy
+import addon_utils
+
+# -----------------------------------------------------------------------------
+# Define error messages/notices & hard coded globals
+# -----------------------------------------------------------------------------
+
+# currently not used
+DEFAULT_TIMEOUT = 10
+DEFAULT_PER_PAGE = 30
+
+
+# -----------------------------------------------------------------------------
+# The main class
+# -----------------------------------------------------------------------------
+
+class Singleton_updater(object):
+ """
+ This is the singleton class to reference a copy from,
+ it is the shared module level class
+ """
+ def __init__(self):
+
+ self._engine = GithubEngine()
+ self._user = None
+ self._repo = None
+ self._website = None
+ self._current_version = None
+ self._subfolder_path = None
+ self._tags = []
+ self._tag_latest = None
+ self._tag_names = []
+ self._latest_release = None
+ self._use_releases = False
+ self._include_branches = False
+ self._include_branch_list = ['master']
+ self._include_branch_autocheck = False
+ self._manual_only = False
+ self._version_min_update = None
+ self._version_max_update = None
+
+ # by default, backup current addon if new is being loaded
+ self._backup_current = True
+ self._backup_ignore_patterns = None
+
+ # set patterns for what files to overwrite on update
+ self._overwrite_patterns = ["*.py","*.pyc"]
+ self._remove_pre_update_patterns = []
+
+ # by default, don't auto enable/disable the addon on update
+ # as it is slightly less stable/won't always fully reload module
+ self._auto_reload_post_update = False
+
+ # settings relating to frequency and whether to enable auto background check
+ self._check_interval_enable = False
+ self._check_interval_months = 0
+ self._check_interval_days = 7
+ self._check_interval_hours = 0
+ self._check_interval_minutes = 0
+
+ # runtime variables, initial conditions
+ self._verbose = False
+ self._fake_install = False
+ self._async_checking = False # only true when async daemon started
+ self._update_ready = None
+ self._update_link = None
+ self._update_version = None
+ self._source_zip = None
+ self._check_thread = None
+ self.skip_tag = None
+ self.select_link = None
+
+ # get from module data
+ self._addon = __package__.lower()
+ self._addon_package = __package__ # must not change
+ self._updater_path = os.path.join(os.path.dirname(__file__),
+ self._addon+"_updater")
+ self._addon_root = os.path.dirname(__file__)
+ self._json = {}
+ self._error = None
+ self._error_msg = None
+ self._prefiltered_tag_count = 0
+
+ # UI code only, ie not used within this module but still useful
+ # properties to have
+
+ # to verify a valid import, in place of placeholder import
+ self.showpopups = True # used in UI to show or not show update popups
+ self.invalidupdater = False
+
+
+ # -------------------------------------------------------------------------
+ # Getters and setters
+ # -------------------------------------------------------------------------
+
+ @property
+ def engine(self):
+ return self._engine.name
+ @engine.setter
+ def engine(self, value):
+ if value.lower()=="github":
+ self._engine = GithubEngine()
+ elif value.lower()=="gitlab":
+ self._engine = GitlabEngine()
+ elif value.lower()=="bitbucket":
+ self._engine = BitbucketEngine()
+ else:
+ raise ValueError("Invalid engine selection")
+
+ @property
+ def private_token(self):
+ return self._engine.token
+ @private_token.setter
+ def private_token(self, value):
+ if value==None:
+ self._engine.token = None
+ else:
+ self._engine.token = str(value)
+
+ @property
+ def addon(self):
+ return self._addon
+ @addon.setter
+ def addon(self, value):
+ self._addon = str(value)
+
+ @property
+ def verbose(self):
+ return self._verbose
+ @verbose.setter
+ def verbose(self, value):
+ try:
+ self._verbose = bool(value)
+ if self._verbose == True:
+ print(self._addon+" updater verbose is enabled")
+ except:
+ raise ValueError("Verbose must be a boolean value")
+
+ @property
+ def include_branches(self):
+ return self._include_branches
+ @include_branches.setter
+ def include_branches(self, value):
+ try:
+ self._include_branches = bool(value)
+ except:
+ raise ValueError("include_branches must be a boolean value")
+
+ @property
+ def use_releases(self):
+ return self._use_releases
+ @use_releases.setter
+ def use_releases(self, value):
+ try:
+ self._use_releases = bool(value)
+ except:
+ raise ValueError("use_releases must be a boolean value")
+
+ @property
+ def include_branch_list(self):
+ return self._include_branch_list
+ @include_branch_list.setter
+ def include_branch_list(self, value):
+ try:
+ if value == None:
+ self._include_branch_list = ['master']
+ elif type(value) != type(['master']) or value==[]:
+ raise ValueError("include_branch_list should be a list of valid branches")
+ else:
+ self._include_branch_list = value
+ except:
+ raise ValueError("include_branch_list should be a list of valid branches")
+
+ @property
+ def overwrite_patterns(self):
+ return self._overwrite_patterns
+ @overwrite_patterns.setter
+ def overwrite_patterns(self, value):
+ if value == None:
+ self._overwrite_patterns = ["*.py","*.pyc"]
+ elif type(value) != type(['']):
+ raise ValueError("overwrite_patterns needs to be in a list format")
+ else:
+ self._overwrite_patterns = value
+
+ @property
+ def remove_pre_update_patterns(self):
+ return self._remove_pre_update_patterns
+ @remove_pre_update_patterns.setter
+ def remove_pre_update_patterns(self, value):
+ if value == None:
+ self._remove_pre_update_patterns = []
+ elif type(value) != type(['']):
+ raise ValueError("remove_pre_update_patterns needs to be in a list format")
+ else:
+ self._remove_pre_update_patterns = value
+
+ # not currently used
+ @property
+ def include_branch_autocheck(self):
+ return self._include_branch_autocheck
+ @include_branch_autocheck.setter
+ def include_branch_autocheck(self, value):
+ try:
+ self._include_branch_autocheck = bool(value)
+ except:
+ raise ValueError("include_branch_autocheck must be a boolean value")
+
+ @property
+ def manual_only(self):
+ return self._manual_only
+ @manual_only.setter
+ def manual_only(self, value):
+ try:
+ self._manual_only = bool(value)
+ except:
+ raise ValueError("manual_only must be a boolean value")
+
+ @property
+ def auto_reload_post_update(self):
+ return self._auto_reload_post_update
+ @auto_reload_post_update.setter
+ def auto_reload_post_update(self, value):
+ try:
+ self._auto_reload_post_update = bool(value)
+ except:
+ raise ValueError("Must be a boolean value")
+
+ @property
+ def fake_install(self):
+ return self._fake_install
+ @fake_install.setter
+ def fake_install(self, value):
+ if type(value) != type(False):
+ raise ValueError("fake_install must be a boolean value")
+ self._fake_install = bool(value)
+
+ @property
+ def user(self):
+ return self._user
+ @user.setter
+ def user(self, value):
+ try:
+ self._user = str(value)
+ except:
+ raise ValueError("User must be a string value")
+
+ @property
+ def json(self):
+ if self._json == {}:
+ self.set_updater_json()
+ return self._json
+
+ @property
+ def repo(self):
+ return self._repo
+ @repo.setter
+ def repo(self, value):
+ try:
+ self._repo = str(value)
+ except:
+ raise ValueError("User must be a string")
+
+ @property
+ def website(self):
+ return self._website
+ @website.setter
+ def website(self, value):
+ if self.check_is_url(value) == False:
+ raise ValueError("Not a valid URL: " + value)
+ self._website = value
+
+ @property
+ def async_checking(self):
+ return self._async_checking
+
+ @property
+ def api_url(self):
+ return self._engine.api_url
+ @api_url.setter
+ def api_url(self, value):
+ if self.check_is_url(value) == False:
+ raise ValueError("Not a valid URL: " + value)
+ self._engine.api_url = value
+
+ @property
+ def stage_path(self):
+ return self._updater_path
+ @stage_path.setter
+ def stage_path(self, value):
+ if value == None:
+ if self._verbose: print("Aborting assigning stage_path, it's null")
+ return
+ elif value != None and not os.path.exists(value):
+ try:
+ os.makedirs(value)
+ except:
+ if self._verbose: print("Error trying to staging path")
+ return
+ self._updater_path = value
+
+ @property
+ def tags(self):
+ if self._tags == []:
+ return []
+ tag_names = []
+ for tag in self._tags:
+ tag_names.append(tag["name"])
+ return tag_names
+
+ @property
+ def tag_latest(self):
+ if self._tag_latest == None:
+ return None
+ return self._tag_latest["name"]
+
+ @property
+ def latest_release(self):
+ if self._releases_latest == None:
+ return None
+ return self._latest_release
+
+ @property
+ def current_version(self):
+ return self._current_version
+
+ @property
+ def subfolder_path(self):
+ return self._subfolder_path
+
+ @subfolder_path.setter
+ def subfolder_path(self, value):
+ self._subfolder_path = value
+
+ @property
+ def update_ready(self):
+ return self._update_ready
+
+ @property
+ def update_version(self):
+ return self._update_version
+
+ @property
+ def update_link(self):
+ return self._update_link
+
+ @current_version.setter
+ def current_version(self, tuple_values):
+ if tuple_values==None:
+ self._current_version = None
+ return
+ elif type(tuple_values) is not tuple:
+ try:
+ tuple(tuple_values)
+ except:
+ raise ValueError(
+ "Not a tuple! current_version must be a tuple of integers")
+ for i in tuple_values:
+ if type(i) is not int:
+ raise ValueError(
+ "Not an integer! current_version must be a tuple of integers")
+ self._current_version = tuple(tuple_values)
+
+ def set_check_interval(self,enable=False,months=0,days=14,hours=0,minutes=0):
+ # enabled = False, default initially will not check against frequency
+ # if enabled, default is then 2 weeks
+
+ if type(enable) is not bool:
+ raise ValueError("Enable must be a boolean value")
+ if type(months) is not int:
+ raise ValueError("Months must be an integer value")
+ if type(days) is not int:
+ raise ValueError("Days must be an integer value")
+ if type(hours) is not int:
+ raise ValueError("Hours must be an integer value")
+ if type(minutes) is not int:
+ raise ValueError("Minutes must be an integer value")
+
+ if enable==False:
+ self._check_interval_enable = False
+ else:
+ self._check_interval_enable = True
+
+ self._check_interval_months = months
+ self._check_interval_days = days
+ self._check_interval_hours = hours
+ self._check_interval_minutes = minutes
+
+ @property
+ def check_interval(self):
+ return (self._check_interval_enable,
+ self._check_interval_months,
+ self._check_interval_days,
+ self._check_interval_hours,
+ self._check_interval_minutes)
+
+ @property
+ def error(self):
+ return self._error
+
+ @property
+ def error_msg(self):
+ return self._error_msg
+
+ @property
+ def version_min_update(self):
+ return self._version_min_update
+ @version_min_update.setter
+ def version_min_update(self, value):
+ if value == None:
+ self._version_min_update = None
+ return
+ if type(value) != type((1,2,3)):
+ raise ValueError("Version minimum must be a tuple")
+ else:
+ # potentially check entries are integers
+ self._version_min_update = value
+
+ @property
+ def version_max_update(self):
+ return self._version_max_update
+ @version_max_update.setter
+ def version_max_update(self, value):
+ if value == None:
+ self._version_max_update = None
+ return
+ if type(value) != type((1,2,3)):
+ raise ValueError("Version maximum must be a tuple")
+ else:
+ # potentially check entries are integers
+ self._version_max_update = value
+
+ @property
+ def backup_current(self):
+ return self._backup_current
+ @backup_current.setter
+ def backup_current(self, value):
+ if value == None:
+ self._backup_current = False
+ return
+ else:
+ self._backup_current = value
+
+ @property
+ def backup_ignore_patterns(self):
+ return self._backup_ignore_patterns
+ @backup_ignore_patterns.setter
+ def backup_ignore_patterns(self, value):
+ if value == None:
+ self._backup_ignore_patterns = None
+ return
+ elif type(value) != type(['list']):
+ raise ValueError("Backup pattern must be in list format")
+ else:
+ self._backup_ignore_patterns = value
+
+ # -------------------------------------------------------------------------
+ # Parameter validation related functions
+ # -------------------------------------------------------------------------
+
+
+ def check_is_url(self, url):
+ if not ("http://" in url or "https://" in url):
+ return False
+ if "." not in url:
+ return False
+ return True
+
+ def get_tag_names(self):
+ tag_names = []
+ self.get_tags(self)
+ for tag in self._tags:
+ tag_names.append(tag["name"])
+ return tag_names
+
+
+ # declare how the class gets printed
+
+ def __repr__(self):
+ return "<Module updater from {a}>".format(a=__file__)
+
+ def __str__(self):
+ return "Updater, with user: {a}, repository: {b}, url: {c}".format(
+ a=self._user,
+ b=self._repo, c=self.form_repo_url())
+
+
+ # -------------------------------------------------------------------------
+ # API-related functions
+ # -------------------------------------------------------------------------
+
+ def form_repo_url(self):
+ return self._engine.form_repo_url(self)
+
+ def form_tags_url(self):
+ return self._engine.form_tags_url(self)
+
+ def form_branch_url(self, branch):
+ return self._engine.form_branch_url(branch, self)
+
+ def get_tags(self):
+ request = self.form_tags_url()
+ if self._verbose: print("Getting tags from server")
+
+ # get all tags, internet call
+ all_tags = self._engine.parse_tags(self.get_api(request), self)
+ if all_tags is not None:
+ self._prefiltered_tag_count = len(all_tags)
+ else:
+ self._prefiltered_tag_count = 0
+ all_tags = []
+
+ # pre-process to skip tags
+ if self.skip_tag != None:
+ self._tags = [tg for tg in all_tags if self.skip_tag(self, tg)==False]
+ else:
+ self._tags = all_tags
+
+ # get additional branches too, if needed, and place in front
+ # Does NO checking here whether branch is valid
+ if self._include_branches == True:
+ temp_branches = self._include_branch_list.copy()
+ temp_branches.reverse()
+ for branch in temp_branches:
+ request = self.form_branch_url(branch)
+ include = {
+ "name":branch.title(),
+ "zipball_url":request
+ }
+ self._tags = [include] + self._tags # append to front
+
+ if self._tags == None:
+ # some error occurred
+ self._tag_latest = None
+ self._tags = []
+ return
+ elif self._prefiltered_tag_count == 0 and self._include_branches == False:
+ self._tag_latest = None
+ if self._error == None: # if not None, could have had no internet
+ self._error = "No releases found"
+ self._error_msg = "No releases or tags found on this repository"
+ if self._verbose: print("No releases or tags found on this repository")
+ elif self._prefiltered_tag_count == 0 and self._include_branches == True:
+ if not self._error: self._tag_latest = self._tags[0]
+ if self._verbose:
+ branch = self._include_branch_list[0]
+ print("{} branch found, no releases".format(branch), self._tags[0])
+ elif (len(self._tags)-len(self._include_branch_list)==0 and self._include_branches==True) \
+ or (len(self._tags)==0 and self._include_branches==False) \
+ and self._prefiltered_tag_count > 0:
+ self._tag_latest = None
+ self._error = "No releases available"
+ self._error_msg = "No versions found within compatible version range"
+ if self._verbose: print("No versions found within compatible version range")
+ else:
+ if self._include_branches == False:
+ self._tag_latest = self._tags[0]
+ if self._verbose: print("Most recent tag found:",self._tags[0]['name'])
+ else:
+ # don't return branch if in list
+ n = len(self._include_branch_list)
+ self._tag_latest = self._tags[n] # guaranteed at least len()=n+1
+ if self._verbose: print("Most recent tag found:",self._tags[n]['name'])
+
+
+ # all API calls to base url
+ def get_raw(self, url):
+ # print("Raw request:", url)
+ request = urllib.request.Request(url)
+ context = ssl._create_unverified_context()
+
+ # setup private request headers if appropriate
+ if self._engine.token != None:
+ if self._engine.name == "gitlab":
+ request.add_header('PRIVATE-TOKEN',self._engine.token)
+ else:
+ if self._verbose: print("Tokens not setup for engine yet")
+
+ # run the request
+ try:
+ result = urllib.request.urlopen(request,context=context)
+ except urllib.error.HTTPError as e:
+ self._error = "HTTP error"
+ self._error_msg = str(e.code)
+ self._update_ready = None
+ except urllib.error.URLError as e:
+ reason = str(e.reason)
+ if "TLSV1_ALERT" in reason or "SSL" in reason:
+ self._error = "Connection rejected, download manually"
+ self._error_msg = reason
+ else:
+ self._error = "URL error, check internet connection"
+ self._error_msg = reason
+ self._update_ready = None
+ return None
+ else:
+ result_string = result.read()
+ result.close()
+ return result_string.decode()
+
+
+ # result of all api calls, decoded into json format
+ def get_api(self, url):
+ # return the json version
+ get = None
+ get = self.get_raw(url)
+ if get != None:
+ try:
+ return json.JSONDecoder().decode(get)
+ except Exception as e:
+ self._error = "API response has invalid JSON format"
+ self._error_msg = str(e.reason)
+ self._update_ready = None
+ return None
+ else:
+ return None
+
+
+ # create a working directory and download the new files
+ def stage_repository(self, url):
+
+ local = os.path.join(self._updater_path,"update_staging")
+ error = None
+
+ # make/clear the staging folder
+ # ensure the folder is always "clean"
+ if self._verbose: print("Preparing staging folder for download:\n",local)
+ if os.path.isdir(local) == True:
+ try:
+ shutil.rmtree(local)
+ os.makedirs(local)
+ except:
+ error = "failed to remove existing staging directory"
+ else:
+ try:
+ os.makedirs(local)
+ except:
+ error = "failed to create staging directory"
+
+ if error != None:
+ if self._verbose: print("Error: Aborting update, "+error)
+ self._error = "Update aborted, staging path error"
+ self._error_msg = "Error: {}".format(error)
+ return False
+
+ if self._backup_current==True:
+ self.create_backup()
+ if self._verbose: print("Now retrieving the new source zip")
+
+ self._source_zip = os.path.join(local,"source.zip")
+
+ if self._verbose: print("Starting download update zip")
+ try:
+ request = urllib.request.Request(url)
+ context = ssl._create_unverified_context()
+
+ # setup private token if appropriate
+ if self._engine.token != None:
+ if self._engine.name == "gitlab":
+ request.add_header('PRIVATE-TOKEN',self._engine.token)
+ else:
+ if self._verbose: print("Tokens not setup for selected engine yet")
+ self.urlretrieve(urllib.request.urlopen(request,context=context), self._source_zip)
+ # add additional checks on file size being non-zero
+ if self._verbose: print("Successfully downloaded update zip")
+ return True
+ except Exception as e:
+ self._error = "Error retrieving download, bad link?"
+ self._error_msg = "Error: {}".format(e)
+ if self._verbose:
+ print("Error retrieving download, bad link?")
+ print("Error: {}".format(e))
+ return False
+
+
+ def create_backup(self):
+ if self._verbose: print("Backing up current addon folder")
+ local = os.path.join(self._updater_path,"backup")
+ tempdest = os.path.join(self._addon_root,
+ os.pardir,
+ self._addon+"_updater_backup_temp")
+
+ if self._verbose: print("Backup destination path: ",local)
+
+ if os.path.isdir(local):
+ try:
+ shutil.rmtree(local)
+ except:
+ if self._verbose:print("Failed to removed previous backup folder, contininuing")
+
+ # remove the temp folder; shouldn't exist but could if previously interrupted
+ if os.path.isdir(tempdest):
+ try:
+ shutil.rmtree(tempdest)
+ except:
+ if self._verbose:print("Failed to remove existing temp folder, contininuing")
+ # make the full addon copy, which temporarily places outside the addon folder
+ if self._backup_ignore_patterns != None:
+ shutil.copytree(
+ self._addon_root,tempdest,
+ ignore=shutil.ignore_patterns(*self._backup_ignore_patterns))
+ else:
+ shutil.copytree(self._addon_root,tempdest)
+ shutil.move(tempdest,local)
+
+ # save the date for future ref
+ now = datetime.now()
+ self._json["backup_date"] = "{m}-{d}-{yr}".format(
+ m=now.strftime("%B"),d=now.day,yr=now.year)
+ self.save_updater_json()
+
+ def restore_backup(self):
+ if self._verbose: print("Restoring backup")
+
+ if self._verbose: print("Backing up current addon folder")
+ backuploc = os.path.join(self._updater_path,"backup")
+ tempdest = os.path.join(self._addon_root,
+ os.pardir,
+ self._addon+"_updater_backup_temp")
+ tempdest = os.path.abspath(tempdest)
+
+ # make the copy
+ shutil.move(backuploc,tempdest)
+ shutil.rmtree(self._addon_root)
+ os.rename(tempdest,self._addon_root)
+
+ self._json["backup_date"] = ""
+ self._json["just_restored"] = True
+ self._json["just_updated"] = True
+ self.save_updater_json()
+
+ self.reload_addon()
+
+ def unpack_staged_zip(self,clean=False):
+
+ if os.path.isfile(self._source_zip) == False:
+ if self._verbose: print("Error, update zip not found")
+ return -1
+
+ # clear the existing source folder in case previous files remain
+ try:
+ shutil.rmtree(os.path.join(self._updater_path,"source"))
+ os.makedirs(os.path.join(self._updater_path,"source"))
+ if self._verbose: print("Source folder cleared and recreated")
+ except:
+ pass
+
+ if self._verbose: print("Begin extracting source")
+ if zipfile.is_zipfile(self._source_zip):
+ with zipfile.ZipFile(self._source_zip) as zf:
+ # extractall is no longer a security hazard, below is safe
+ zf.extractall(os.path.join(self._updater_path,"source"))
+ else:
+ if self._verbose:
+ print("Not a zip file, future add support for just .py files")
+ raise ValueError("Resulting file is not a zip")
+ if self._verbose: print("Extracted source")
+
+ # either directly in root of zip, or one folder level deep
+ unpath = os.path.join(self._updater_path,"source")
+ if os.path.isfile(os.path.join(unpath,"__init__.py")) == False:
+ dirlist = os.listdir(unpath)
+ if len(dirlist)>0:
+ if self._subfolder_path == "" or self._subfolder_path == None:
+ unpath = os.path.join(unpath,dirlist[0])
+ else:
+ unpath = os.path.join(unpath,dirlist[0],self._subfolder_path)
+
+ # smarter check for additional sub folders for a single folder
+ # containing __init__.py
+ if os.path.isfile(os.path.join(unpath,"__init__.py")) == False:
+ if self._verbose:
+ print("not a valid addon found")
+ print("Paths:")
+ print(dirlist)
+
+ raise ValueError("__init__ file not found in new source")
+
+ # now commence merging in the two locations:
+ # note this MAY not be accurate, as updater files could be placed elsewhere
+ origpath = os.path.dirname(__file__)
+
+ # merge code with running addon directory, using blender default behavior
+ # plus any modifiers indicated by user (e.g. force remove/keep)
+ self.deepMergeDirectory(origpath,unpath,clean)
+
+ # Now save the json state
+ # Change to True, to trigger the handler on other side
+ # if allowing reloading within same blender instance
+ self._json["just_updated"] = True
+ self.save_updater_json()
+ self.reload_addon()
+ self._update_ready = False
+
+
+ # merge folder 'merger' into folder 'base' without deleting existing
+ def deepMergeDirectory(self,base,merger,clean=False):
+ if not os.path.exists(base):
+ if self._verbose: print("Base path does not exist")
+ return -1
+ elif not os.path.exists(merger):
+ if self._verbose: print("Merger path does not exist")
+ return -1
+
+ # paths to be aware of and not overwrite/remove/etc
+ staging_path = os.path.join(self._updater_path,"update_staging")
+ backup_path = os.path.join(self._updater_path,"backup")
+ json_path = os.path.join(self._updater_path,"updater_status.json")
+
+ # If clean install is enabled, clear existing files ahead of time
+ # note: will not delete the update.json, update folder, staging, or staging
+ # but will delete all other folders/files in addon directory
+ error = None
+ if clean==True:
+ try:
+ # implement clearing of all folders/files, except the
+ # updater folder and updater json
+ # Careful, this deletes entire subdirectories recursively...
+ # make sure that base is not a high level shared folder, but
+ # is dedicated just to the addon itself
+ if self._verbose: print("clean=True, clearing addon folder to fresh install state")
+
+ # remove root files and folders (except update folder)
+ files = [f for f in os.listdir(base) if os.path.isfile(os.path.join(base,f))]
+ folders = [f for f in os.listdir(base) if os.path.isdir(os.path.join(base,f))]
+
+ for f in files:
+ os.remove(os.path.join(base,f))
+ print("Clean removing file {}".format(os.path.join(base,f)))
+ for f in folders:
+ if os.path.join(base,f)==self._updater_path: continue
+ shutil.rmtree(os.path.join(base,f))
+ print("Clean removing folder and contents {}".format(os.path.join(base,f)))
+
+ except error:
+ error = "failed to create clean existing addon folder"
+ print(error,str(e))
+
+ # Walk through the base addon folder for rules on pre-removing
+ # but avoid removing/altering backup and updater file
+ for path, dirs, files in os.walk(base):
+ # prune ie skip updater folder
+ dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]]
+ for file in files:
+ for ptrn in self.remove_pre_update_patterns:
+ if fnmatch.filter([file],ptrn):
+ try:
+ fl = os.path.join(path,file)
+ os.remove(fl)
+ if self._verbose: print("Pre-removed file "+file)
+ except OSError:
+ print("Failed to pre-remove "+file)
+
+ # Walk through the temp addon sub folder for replacements
+ # this implements the overwrite rules, which apply after
+ # the above pre-removal rules. This also performs the
+ # actual file copying/replacements
+ for path, dirs, files in os.walk(merger):
+ # verify this structure works to prune updater sub folder overwriting
+ dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]]
+ relPath = os.path.relpath(path, merger)
+ destPath = os.path.join(base, relPath)
+ if not os.path.exists(destPath):
+ os.makedirs(destPath)
+ for file in files:
+ # bring in additional logic around copying/replacing
+ # Blender default: overwrite .py's, don't overwrite the rest
+ destFile = os.path.join(destPath, file)
+ srcFile = os.path.join(path, file)
+
+ # decide whether to replace if file already exists, and copy new over
+ if os.path.isfile(destFile):
+ # otherwise, check each file to see if matches an overwrite pattern
+ replaced=False
+ for ptrn in self._overwrite_patterns:
+ if fnmatch.filter([destFile],ptrn):
+ replaced=True
+ break
+ if replaced:
+ os.remove(destFile)
+ os.rename(srcFile, destFile)
+ if self._verbose: print("Overwrote file "+os.path.basename(destFile))
+ else:
+ if self._verbose: print("Pattern not matched to "+os.path.basename(destFile)+", not overwritten")
+ else:
+ # file did not previously exist, simply move it over
+ os.rename(srcFile, destFile)
+ if self._verbose: print("New file "+os.path.basename(destFile))
+
+ # now remove the temp staging folder and downloaded zip
+ try:
+ shutil.rmtree(staging_path)
+ except:
+ error = "Error: Failed to remove existing staging directory, consider manually removing "+staging_path
+ if self._verbose: print(error)
+
+
+ def reload_addon(self):
+ # if post_update false, skip this function
+ # else, unload/reload addon & trigger popup
+ if self._auto_reload_post_update == False:
+ print("Restart blender to reload addon and complete update")
+ return
+
+ if self._verbose: print("Reloading addon...")
+ addon_utils.modules(refresh=True)
+ bpy.utils.refresh_script_paths()
+
+ # not allowed in restricted context, such as register module
+ # toggle to refresh
+ bpy.ops.wm.addon_disable(module=self._addon_package)
+ bpy.ops.wm.addon_refresh()
+ bpy.ops.wm.addon_enable(module=self._addon_package)
+
+
+ # -------------------------------------------------------------------------
+ # Other non-api functions and setups
+ # -------------------------------------------------------------------------
+
+ def clear_state(self):
+ self._update_ready = None
+ self._update_link = None
+ self._update_version = None
+ self._source_zip = None
+ self._error = None
+ self._error_msg = None
+
+ # custom urlretrieve implementation
+ def urlretrieve(self, urlfile, filepath):
+ chunk = 1024*8
+ f = open(filepath, "wb")
+ while 1:
+ data = urlfile.read(chunk)
+ if not data:
+ #print("done.")
+ break
+ f.write(data)
+ #print("Read %s bytes"%len(data))
+ f.close()
+
+
+ def version_tuple_from_text(self,text):
+ if text == None: return ()
+
+ # should go through string and remove all non-integers,
+ # and for any given break split into a different section
+ segments = []
+ tmp = ''
+ for l in str(text):
+ if l.isdigit()==False:
+ if len(tmp)>0:
+ segments.append(int(tmp))
+ tmp = ''
+ else:
+ tmp+=l
+ if len(tmp)>0:
+ segments.append(int(tmp))
+
+ if len(segments)==0:
+ if self._verbose: print("No version strings found text: ",text)
+ if self._include_branches == False:
+ return ()
+ else:
+ return (text)
+ return tuple(segments)
+
+ # called for running check in a background thread
+ def check_for_update_async(self, callback=None):
+
+ if self._json != None and "update_ready" in self._json and self._json["version_text"]!={}:
+ if self._json["update_ready"] == True:
+ self._update_ready = True
+ self._update_link = self._json["version_text"]["link"]
+ self._update_version = str(self._json["version_text"]["version"])
+ # cached update
+ callback(True)
+ return
+
+ # do the check
+ if self._check_interval_enable == False:
+ return
+ elif self._async_checking == True:
+ if self._verbose: print("Skipping async check, already started")
+ return # already running the bg thread
+ elif self._update_ready == None:
+ self.start_async_check_update(False, callback)
+
+
+ def check_for_update_now(self, callback=None):
+
+ self._error = None
+ self._error_msg = None
+
+ if self._verbose:
+ print("Check update pressed, first getting current status")
+ if self._async_checking == True:
+ if self._verbose: print("Skipping async check, already started")
+ return # already running the bg thread
+ elif self._update_ready == None:
+ self.start_async_check_update(True, callback)
+ else:
+ self._update_ready = None
+ self.start_async_check_update(True, callback)
+
+
+ # this function is not async, will always return in sequential fashion
+ # but should have a parent which calls it in another thread
+ def check_for_update(self, now=False):
+ if self._verbose: print("Checking for update function")
+
+ # clear the errors if any
+ self._error = None
+ self._error_msg = None
+
+ # avoid running again in, just return past result if found
+ # but if force now check, then still do it
+ if self._update_ready != None and now == False:
+ return (self._update_ready,self._update_version,self._update_link)
+
+ if self._current_version == None:
+ raise ValueError("current_version not yet defined")
+ if self._repo == None:
+ raise ValueError("repo not yet defined")
+ if self._user == None:
+ raise ValueError("username not yet defined")
+
+ self.set_updater_json() # self._json
+
+ if now == False and self.past_interval_timestamp()==False:
+ if self._verbose:
+ print("Aborting check for updated, check interval not reached")
+ return (False, None, None)
+
+ # check if using tags or releases
+ # note that if called the first time, this will pull tags from online
+ if self._fake_install == True:
+ if self._verbose:
+ print("fake_install = True, setting fake version as ready")
+ self._update_ready = True
+ self._update_version = "(999,999,999)"
+ self._update_link = "http://127.0.0.1"
+
+ return (self._update_ready, self._update_version, self._update_link)
+
+ # primary internet call
+ self.get_tags() # sets self._tags and self._tag_latest
+
+ self._json["last_check"] = str(datetime.now())
+ self.save_updater_json()
+
+ # can be () or ('master') in addition to branches, and version tag
+ new_version = self.version_tuple_from_text(self.tag_latest)
+
+ if len(self._tags)==0:
+ self._update_ready = False
+ self._update_version = None
+ self._update_link = None
+ return (False, None, None)
+ if self._include_branches == False:
+ link = self.select_link(self, self._tags[0])
+ else:
+ n = len(self._include_branch_list)
+ if len(self._tags)==n:
+ # effectively means no tags found on repo
+ # so provide the first one as default
+ link = self.select_link(self, self._tags[0])
+ else:
+ link = self.select_link(self, self._tags[n])
+
+ if new_version == ():
+ self._update_ready = False
+ self._update_version = None
+ self._update_link = None
+ return (False, None, None)
+ elif str(new_version).lower() in self._include_branch_list:
+ # handle situation where master/whichever branch is included
+ # however, this code effectively is not triggered now
+ # as new_version will only be tag names, not branch names
+ if self._include_branch_autocheck == False:
+ # don't offer update as ready,
+ # but set the link for the default
+ # branch for installing
+ self._update_ready = True
+ self._update_version = new_version
+ self._update_link = link
+ self.save_updater_json()
+ return (True, new_version, link)
+ else:
+ raise ValueError("include_branch_autocheck: NOT YET DEVELOPED")
+ # bypass releases and look at timestamp of last update
+ # from a branch compared to now, see if commit values
+ # match or not.
+
+ else:
+ # situation where branches not included
+
+ if new_version > self._current_version:
+
+ self._update_ready = True
+ self._update_version = new_version
+ self._update_link = link
+ self.save_updater_json()
+ return (True, new_version, link)
+
+ # elif new_version != self._current_version:
+ # self._update_ready = False
+ # self._update_version = new_version
+ # self._update_link = link
+ # self.save_updater_json()
+ # return (True, new_version, link)
+
+ # if no update, set ready to False from None
+ self._update_ready = False
+ self._update_version = None
+ self._update_link = None
+ return (False, None, None)
+
+
+ def set_tag(self,name):
+ tg = None
+ for tag in self._tags:
+ if name == tag["name"]:
+ tg = tag
+ break
+ if tg == None:
+ raise ValueError("Version tag not found: "+revert_tag)
+ new_version = self.version_tuple_from_text(self.tag_latest)
+ self._update_version = new_version
+ self._update_link = self.select_link(self, tg)
+
+
+ def run_update(self,force=False,revert_tag=None,clean=False,callback=None):
+ # revert_tag: could e.g. get from drop down list
+ # different versions of the addon to revert back to
+ # clean: not used, but in future could use to totally refresh addon
+ self._json["update_ready"] = False
+ self._json["ignore"] = False # clear ignore flag
+ self._json["version_text"] = {}
+
+ if revert_tag != None:
+ self.set_tag(revert_tag)
+ self._update_ready = True
+
+ # clear the errors if any
+ self._error = None
+ self._error_msg = None
+
+ if self._verbose: print("Running update")
+
+ if self._fake_install == True:
+ # change to True, to trigger the reload/"update installed" handler
+ if self._verbose:
+ print("fake_install=True")
+ print("Just reloading and running any handler triggers")
+ self._json["just_updated"] = True
+ self.save_updater_json()
+ if self._backup_current == True:
+ self.create_backup()
+ self.reload_addon()
+ self._update_ready = False
+ res = True # fake "success" zip download flag
+
+ elif force==False:
+ if self._update_ready != True:
+ if self._verbose: print("Update stopped, new version not ready")
+ return "Update stopped, new version not ready"
+ elif self._update_link == None:
+ # this shouldn't happen if update is ready
+ if self._verbose: print("Update stopped, update link unavailable")
+ return "Update stopped, update link unavailable"
+
+ if self._verbose and revert_tag==None:
+ print("Staging update")
+ elif self._verbose:
+ print("Staging install")
+
+ res = self.stage_repository(self._update_link)
+ if res !=True:
+ print("Error in staging repository: "+str(res))
+ if callback != None: callback(self._error_msg)
+ return self._error_msg
+ self.unpack_staged_zip(clean)
+
+ else:
+ if self._update_link == None:
+ if self._verbose: print("Update stopped, could not get link")
+ return "Update stopped, could not get link"
+ if self._verbose: print("Forcing update")
+
+ res = self.stage_repository(self._update_link)
+ if res !=True:
+ print("Error in staging repository: "+str(res))
+ if callback != None: callback(self._error_msg)
+ return self._error_msg
+ self.unpack_staged_zip(clean)
+ # would need to compare against other versions held in tags
+
+ # run the front-end's callback if provided
+ if callback != None: callback()
+
+ # return something meaningful, 0 means it worked
+ return 0
+
+
+ def past_interval_timestamp(self):
+ if self._check_interval_enable == False:
+ return True # ie this exact feature is disabled
+
+ if "last_check" not in self._json or self._json["last_check"] == "":
+ return True
+ else:
+ now = datetime.now()
+ last_check = datetime.strptime(self._json["last_check"],
+ "%Y-%m-%d %H:%M:%S.%f")
+ next_check = last_check
+ offset = timedelta(
+ days=self._check_interval_days + 30*self._check_interval_months,
+ hours=self._check_interval_hours,
+ minutes=self._check_interval_minutes
+ )
+
+ delta = (now - offset) - last_check
+ if delta.total_seconds() > 0:
+ if self._verbose:
+ print("{} Updater: Time to check for updates!".format(self._addon))
+ return True
+ else:
+ if self._verbose:
+ print("{} Updater: Determined it's not yet time to check for updates".format(self._addon))
+ return False
+
+
+ def set_updater_json(self):
+ if self._updater_path == None:
+ raise ValueError("updater_path is not defined")
+ elif os.path.isdir(self._updater_path) == False:
+ os.makedirs(self._updater_path)
+
+ jpath = os.path.join(self._updater_path,"updater_status.json")
+ if os.path.isfile(jpath):
+ with open(jpath) as data_file:
+ self._json = json.load(data_file)
+ if self._verbose: print("{} Updater: Read in json settings from file".format(self._addon))
+ else:
+ # set data structure
+ self._json = {
+ "last_check":"",
+ "backup_date":"",
+ "update_ready":False,
+ "ignore":False,
+ "just_restored":False,
+ "just_updated":False,
+ "version_text":{}
+ }
+ self.save_updater_json()
+
+
+ def save_updater_json(self):
+ # first save the state
+ if self._update_ready == True:
+ if type(self._update_version) == type((0,0,0)):
+ self._json["update_ready"] = True
+ self._json["version_text"]["link"]=self._update_link
+ self._json["version_text"]["version"]=self._update_version
+ else:
+ self._json["update_ready"] = False
+ self._json["version_text"] = {}
+ else:
+ self._json["update_ready"] = False
+ self._json["version_text"] = {}
+
+ jpath = os.path.join(self._updater_path,"updater_status.json")
+ outf = open(jpath,'w')
+ data_out = json.dumps(self._json, indent=4)
+ outf.write(data_out)
+ outf.close()
+ if self._verbose:
+ print(self._addon+": Wrote out updater json settings to file, with the contents:")
+ print(self._json)
+
+ def json_reset_postupdate(self):
+ self._json["just_updated"] = False
+ self._json["update_ready"] = False
+ self._json["version_text"] = {}
+ self.save_updater_json()
+
+ def json_reset_restore(self):
+ self._json["just_restored"] = False
+ self._json["update_ready"] = False
+ self._json["version_text"] = {}
+ self.save_updater_json()
+ self._update_ready = None # reset so you could check update again
+
+ def ignore_update(self):
+ self._json["ignore"] = True
+ self.save_updater_json()
+
+
+ # -------------------------------------------------------------------------
+ # ASYNC stuff
+ # -------------------------------------------------------------------------
+
+ def start_async_check_update(self, now=False, callback=None):
+ if self._async_checking == True:
+ return
+ if self._verbose: print("{} updater: Starting background checking thread".format(self._addon))
+ check_thread = threading.Thread(target=self.async_check_update,
+ args=(now,callback,))
+ check_thread.daemon = True
+ self._check_thread = check_thread
+ check_thread.start()
+
+ return True
+
+ def async_check_update(self, now, callback=None):
+ self._async_checking = True
+ if self._verbose: print("{} BG thread: Checking for update now in background".format(self._addon))
+ # time.sleep(3) # to test background, in case internet too fast to tell
+ # try:
+ self.check_for_update(now=now)
+ # except Exception as exception:
+ # print("Checking for update error:")
+ # print(exception)
+ # self._update_ready = False
+ # self._update_version = None
+ # self._update_link = None
+ # self._error = "Error occurred"
+ # self._error_msg = "Encountered an error while checking for updates"
+
+ self._async_checking = False
+ self._check_thread = None
+
+ if self._verbose:
+ print("{} BG thread: Finished checking for update, doing callback".format(self._addon))
+ if callback != None: callback(self._update_ready)
+
+
+ def stop_async_check_update(self):
+ if self._check_thread != None:
+ try:
+ if self._verbose: print("Thread will end in normal course.")
+ # however, "There is no direct kill method on a thread object."
+ # better to let it run its course
+ #self._check_thread.stop()
+ except:
+ pass
+ self._async_checking = False
+ self._error = None
+ self._error_msg = None
+
+
+# -----------------------------------------------------------------------------
+# Updater Engines
+# -----------------------------------------------------------------------------
+
+
+class BitbucketEngine(object):
+
+ def __init__(self):
+ self.api_url = 'https://api.bitbucket.org'
+ self.token = None
+ self.name = "bitbucket"
+
+ def form_repo_url(self, updater):
+ return self.api_url+"/2.0/repositories/"+updater.user+"/"+updater.repo
+
+ def form_tags_url(self, updater):
+ return self.form_repo_url(updater) + "/refs/tags?sort=-name"
+
+ def form_branch_url(self, branch, updater):
+ return self.get_zip_url(branch, updater)
+
+ def get_zip_url(self, name, updater):
+ return "https://bitbucket.org/{user}/{repo}/get/{name}.zip".format(
+ user=updater.user,
+ repo=updater.repo,
+ name=name)
+
+ def parse_tags(self, response, updater):
+ if response == None:
+ return []
+ return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["name"], updater)} for tag in response["values"]]
+
+
+class GithubEngine(object):
+
+ def __init__(self):
+ self.api_url = 'https://api.github.com'
+ self.token = None
+ self.name = "github"
+
+ def form_repo_url(self, updater):
+ return "{}{}{}{}{}".format(self.api_url,"/repos/",updater.user,
+ "/",updater.repo)
+
+ def form_tags_url(self, updater):
+ if updater.use_releases:
+ return "{}{}".format(self.form_repo_url(updater),"/releases")
+ else:
+ return "{}{}".format(self.form_repo_url(updater),"/tags")
+
+ def form_branch_list_url(self, updater):
+ return "{}{}".format(self.form_repo_url(updater),"/branches")
+
+ def form_branch_url(self, branch, updater):
+ return "{}{}{}".format(self.form_repo_url(updater),
+ "/zipball/",branch)
+
+ def parse_tags(self, response, updater):
+ if response == None:
+ return []
+ return response
+
+
+class GitlabEngine(object):
+
+ def __init__(self):
+ self.api_url = 'https://gitlab.com'
+ self.token = None
+ self.name = "gitlab"
+
+ def form_repo_url(self, updater):
+ return "{}{}{}".format(self.api_url,"/api/v3/projects/",updater.repo)
+
+ def form_tags_url(self, updater):
+ return "{}{}".format(self.form_repo_url(updater),"/repository/tags")
+
+ def form_branch_list_url(self, updater):
+ # does not validate branch name.
+ return "{}{}".format(
+ self.form_repo_url(updater),
+ "/repository/branches")
+
+ def form_branch_url(self, branch, updater):
+ # Could clash with tag names and if it does, it will
+ # download TAG zip instead of branch zip to get
+ # direct path, would need.
+ return "{}{}{}".format(
+ self.form_repo_url(updater),
+ "/repository/archive.zip?sha=",
+ branch)
+
+ def get_zip_url(self, sha, updater):
+ return "{base}/repository/archive.zip?sha:{sha}".format(
+ base=self.form_repo_url(updater),
+ sha=sha)
+
+ # def get_commit_zip(self, id, updater):
+ # return self.form_repo_url(updater)+"/repository/archive.zip?sha:"+id
+
+ def parse_tags(self, response, updater):
+ if response == None:
+ return []
+ return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["commit"]["id"], updater)} for tag in response]
+
+
+# -----------------------------------------------------------------------------
+# The module-shared class instance,
+# should be what's imported to other files
+# -----------------------------------------------------------------------------
+
+Updater = Singleton_updater()
diff --git a/uv_magic_uv/addon_updater_ops.py b/uv_magic_uv/addon_updater_ops.py
new file mode 100644
index 00000000..418334ad
--- /dev/null
+++ b/uv_magic_uv/addon_updater_ops.py
@@ -0,0 +1,1357 @@
+# ##### 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 #####
+
+import bpy
+from bpy.app.handlers import persistent
+import os
+
+# updater import, import safely
+# Prevents popups for users with invalid python installs e.g. missing libraries
+try:
+ from .addon_updater import Updater as updater
+except Exception as e:
+ print("ERROR INITIALIZING UPDATER")
+ print(str(e))
+ class Singleton_updater_none(object):
+ def __init__(self):
+ self.addon = None
+ self.verbose = False
+ self.invalidupdater = True # used to distinguish bad install
+ self.error = None
+ self.error_msg = None
+ self.async_checking = None
+ def clear_state(self):
+ self.addon = None
+ self.verbose = False
+ self.invalidupdater = True
+ self.error = None
+ self.error_msg = None
+ self.async_checking = None
+ def run_update(self): pass
+ def check_for_update(self): pass
+ updater = Singleton_updater_none()
+ updater.error = "Error initializing updater module"
+ updater.error_msg = str(e)
+
+# Must declare this before classes are loaded
+# otherwise the bl_idname's will not match and have errors.
+# Must be all lowercase and no spaces
+updater.addon = "magic_uv"
+
+dispaly_addon_name = "Magic UV"
+
+# -----------------------------------------------------------------------------
+# Updater operators
+# -----------------------------------------------------------------------------
+
+
+# simple popup for prompting checking for update & allow to install if available
+class addon_updater_install_popup(bpy.types.Operator):
+ """Check and install update if available"""
+ bl_label = "Update {x} addon".format(x=updater.addon)
+ bl_idname = updater.addon+".updater_install_popup"
+ bl_description = "Popup menu to check and display current updates available"
+ bl_options = {'REGISTER', 'INTERNAL'}
+
+ # if true, run clean install - ie remove all files before adding new
+ # equivalent to deleting the addon and reinstalling, except the
+ # updater folder/backup folder remains
+ clean_install = bpy.props.BoolProperty(
+ name="Clean install",
+ description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
+ default=False,
+ options={'HIDDEN'}
+ )
+ ignore_enum = bpy.props.EnumProperty(
+ name="Process update",
+ description="Decide to install, ignore, or defer new addon update",
+ items=[
+ ("install","Update Now","Install update now"),
+ ("ignore","Ignore", "Ignore this update to prevent future popups"),
+ ("defer","Defer","Defer choice till next blender session")
+ ],
+ options={'HIDDEN'}
+ )
+
+ def check (self, context):
+ return True
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_props_dialog(self)
+
+ def draw(self, context):
+ layout = self.layout
+ if updater.invalidupdater == True:
+ layout.label("Updater module error")
+ return
+ elif updater.update_ready == True:
+ col = layout.column()
+ col.scale_y = 0.7
+ col.label("Update {} ready!".format(str(updater.update_version)),
+ icon="LOOP_FORWARDS")
+ col.label("Choose 'Update Now' & press OK to install, ",icon="BLANK1")
+ col.label("or click outside window to defer",icon="BLANK1")
+ row = col.row()
+ row.prop(self,"ignore_enum",expand=True)
+ col.split()
+ elif updater.update_ready == False:
+ col = layout.column()
+ col.scale_y = 0.7
+ col.label("No updates available")
+ col.label("Press okay to dismiss dialog")
+ # add option to force install
+ else:
+ # case: updater.update_ready = None
+ # we have not yet checked for the update
+ layout.label("Check for update now?")
+
+ # potentially in future, could have UI for 'check to select old version'
+ # to revert back to.
+
+ def execute(self,context):
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return {'CANCELLED'}
+
+ if updater.manual_only==True:
+ bpy.ops.wm.url_open(url=updater.website)
+ elif updater.update_ready == True:
+
+ # action based on enum selection
+ if self.ignore_enum=='defer':
+ return {'FINISHED'}
+ elif self.ignore_enum=='ignore':
+ updater.ignore_update()
+ return {'FINISHED'}
+ #else: "install update now!"
+
+ res = updater.run_update(
+ force=False,
+ callback=post_update_callback,
+ clean=self.clean_install)
+ # should return 0, if not something happened
+ if updater.verbose:
+ if res==0: print("Updater returned successful")
+ else: print("Updater returned "+str(res)+", error occurred")
+ elif updater.update_ready == None:
+ (update_ready, version, link) = updater.check_for_update(now=True)
+
+ # re-launch this dialog
+ atr = addon_updater_install_popup.bl_idname.split(".")
+ getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
+ else:
+ if updater.verbose:print("Doing nothing, not ready for update")
+ return {'FINISHED'}
+
+
+# User preference check-now operator
+class addon_updater_check_now(bpy.types.Operator):
+ bl_label = "Check now for "+dispaly_addon_name+" update"
+ bl_idname = updater.addon+".updater_check_now"
+ bl_description = "Check now for an update to the {x} addon".format(
+ x=updater.addon)
+ bl_options = {'REGISTER', 'INTERNAL'}
+
+ def execute(self,context):
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return {'CANCELLED'}
+
+ if updater.async_checking == True and updater.error == None:
+ # Check already happened
+ # Used here to just avoid constant applying settings below
+ # Ignoring if error, to prevent being stuck on the error screen
+ return {'CANCELLED'}
+
+ # apply the UI settings
+ settings = context.user_preferences.addons[__package__].preferences
+ updater.set_check_interval(enable=settings.auto_check_update,
+ months=settings.updater_intrval_months,
+ days=settings.updater_intrval_days,
+ hours=settings.updater_intrval_hours,
+ minutes=settings.updater_intrval_minutes
+ ) # optional, if auto_check_update
+
+ # input is an optional callback function
+ # this function should take a bool input, if true: update ready
+ # if false, no update ready
+ updater.check_for_update_now(ui_refresh)
+
+ return {'FINISHED'}
+
+
+class addon_updater_update_now(bpy.types.Operator):
+ bl_label = "Update "+updater.addon+" addon now"
+ bl_idname = updater.addon+".updater_update_now"
+ bl_description = "Update to the latest version of the {x} addon".format(
+ x=updater.addon)
+ bl_options = {'REGISTER', 'INTERNAL'}
+
+ # if true, run clean install - ie remove all files before adding new
+ # equivalent to deleting the addon and reinstalling, except the
+ # updater folder/backup folder remains
+ clean_install = bpy.props.BoolProperty(
+ name="Clean install",
+ description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
+ default=False,
+ options={'HIDDEN'}
+ )
+
+ def execute(self,context):
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return {'CANCELLED'}
+
+ if updater.manual_only == True:
+ bpy.ops.wm.url_open(url=updater.website)
+ if updater.update_ready == True:
+ # if it fails, offer to open the website instead
+ try:
+ res = updater.run_update(
+ force=False,
+ callback=post_update_callback,
+ clean=self.clean_install)
+
+ # should return 0, if not something happened
+ if updater.verbose:
+ if res==0: print("Updater returned successful")
+ else: print("Updater returned "+str(res)+", error occurred")
+ except Exception as e:
+ updater._error = "Error trying to run update"
+ updater._error_msg = str(e)
+ atr = addon_updater_install_manually.bl_idname.split(".")
+ getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
+ elif updater.update_ready == None:
+ (update_ready, version, link) = updater.check_for_update(now=True)
+ # re-launch this dialog
+ atr = addon_updater_install_popup.bl_idname.split(".")
+ getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
+
+ elif updater.update_ready == False:
+ self.report({'INFO'}, "Nothing to update")
+ else:
+ self.report({'ERROR'}, "Encountered problem while trying to update")
+
+ return {'FINISHED'}
+
+
+class addon_updater_update_target(bpy.types.Operator):
+ bl_label = updater.addon+" addon version target"
+ bl_idname = updater.addon+".updater_update_target"
+ bl_description = "Install a targeted version of the {x} addon".format(
+ x=updater.addon)
+ bl_options = {'REGISTER', 'INTERNAL'}
+
+ def target_version(self, context):
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ ret = []
+
+ ret = []
+ i=0
+ for tag in updater.tags:
+ ret.append( (tag,tag,"Select to install "+tag) )
+ i+=1
+ return ret
+
+ target = bpy.props.EnumProperty(
+ name="Target version to install",
+ description="Select the version to install",
+ items=target_version
+ )
+
+ # if true, run clean install - ie remove all files before adding new
+ # equivalent to deleting the addon and reinstalling, except the
+ # updater folder/backup folder remains
+ clean_install = bpy.props.BoolProperty(
+ name="Clean install",
+ description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
+ default=False,
+ options={'HIDDEN'}
+ )
+
+ @classmethod
+ def poll(cls, context):
+ if updater.invalidupdater == True: return False
+ return updater.update_ready != None and len(updater.tags)>0
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_props_dialog(self)
+
+ def draw(self, context):
+ layout = self.layout
+ if updater.invalidupdater == True:
+ layout.label("Updater error")
+ return
+ split = layout.split(percentage=0.66)
+ subcol = split.column()
+ subcol.label("Select install version")
+ subcol = split.column()
+ subcol.prop(self, "target", text="")
+
+
+ def execute(self,context):
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return {'CANCELLED'}
+
+ res = updater.run_update(
+ force=False,
+ revert_tag=self.target,
+ callback=post_update_callback,
+ clean=self.clean_install)
+
+ # should return 0, if not something happened
+ if updater.verbose:
+ if res==0: print("Updater returned successful")
+ else: print("Updater returned "+str(res)+", error occurred")
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+
+class addon_updater_install_manually(bpy.types.Operator):
+ """As a fallback, direct the user to download the addon manually"""
+ bl_label = "Install update manually"
+ bl_idname = updater.addon+".updater_install_manually"
+ bl_description = "Proceed to manually install update"
+ bl_options = {'REGISTER', 'INTERNAL'}
+
+ error = bpy.props.StringProperty(
+ name="Error Occurred",
+ default="",
+ options={'HIDDEN'}
+ )
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_popup(self)
+
+ def draw(self, context):
+ layout = self.layout
+
+ if updater.invalidupdater == True:
+ layout.label("Updater error")
+ return
+
+ # use a "failed flag"? it shows this label if the case failed.
+ if self.error!="":
+ col = layout.column()
+ col.scale_y = 0.7
+ col.label("There was an issue trying to auto-install",icon="ERROR")
+ col.label("Press the download button below and install",icon="BLANK1")
+ col.label("the zip file like a normal addon.",icon="BLANK1")
+ else:
+ col = layout.column()
+ col.scale_y = 0.7
+ col.label("Install the addon manually")
+ col.label("Press the download button below and install")
+ col.label("the zip file like a normal addon.")
+
+ # if check hasn't happened, i.e. accidentally called this menu
+ # allow to check here
+
+ row = layout.row()
+
+ if updater.update_link != None:
+ row.operator("wm.url_open",text="Direct download").url=\
+ updater.update_link
+ else:
+ row.operator("wm.url_open",text="(failed to retrieve direct download)")
+ row.enabled = False
+
+ if updater.website != None:
+ row = layout.row()
+ row.operator("wm.url_open",text="Open website").url=\
+ updater.website
+ else:
+ row = layout.row()
+ row.label("See source website to download the update")
+
+ def execute(self,context):
+
+ return {'FINISHED'}
+
+
+class addon_updater_updated_successful(bpy.types.Operator):
+ """Addon in place, popup telling user it completed or what went wrong"""
+ bl_label = "Installation Report"
+ bl_idname = updater.addon+".updater_update_successful"
+ bl_description = "Update installation response"
+ bl_options = {'REGISTER', 'INTERNAL', 'UNDO'}
+
+ error = bpy.props.StringProperty(
+ name="Error Occurred",
+ default="",
+ options={'HIDDEN'}
+ )
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_props_popup(self, event)
+
+ def draw(self, context):
+ layout = self.layout
+
+ if updater.invalidupdater == True:
+ layout.label("Updater error")
+ return
+
+ saved = updater.json
+ if self.error != "":
+ col = layout.column()
+ col.scale_y = 0.7
+ col.label("Error occurred, did not install", icon="ERROR")
+ col.label(updater.error_msg, icon="BLANK1")
+ rw = col.row()
+ rw.scale_y = 2
+ rw.operator("wm.url_open",
+ text="Click for manual download.",
+ icon="BLANK1"
+ ).url=updater.website
+ # manual download button here
+ elif updater.auto_reload_post_update == False:
+ # tell user to restart blender
+ if "just_restored" in saved and saved["just_restored"] == True:
+ col = layout.column()
+ col.scale_y = 0.7
+ col.label("Addon restored", icon="RECOVER_LAST")
+ col.label("Restart blender to reload.",icon="BLANK1")
+ updater.json_reset_restore()
+ else:
+ col = layout.column()
+ col.scale_y = 0.7
+ col.label("Addon successfully installed", icon="FILE_TICK")
+ col.label("Restart blender to reload.", icon="BLANK1")
+
+ else:
+ # reload addon, but still recommend they restart blender
+ if "just_restored" in saved and saved["just_restored"] == True:
+ col = layout.column()
+ col.scale_y = 0.7
+ col.label("Addon restored", icon="RECOVER_LAST")
+ col.label("Consider restarting blender to fully reload.",icon="BLANK1")
+ updater.json_reset_restore()
+ else:
+ col = layout.column()
+ col.scale_y = 0.7
+ col.label("Addon successfully installed", icon="FILE_TICK")
+ col.label("Consider restarting blender to fully reload.", icon="BLANK1")
+
+ def execut(self, context):
+ return {'FINISHED'}
+
+
+class addon_updater_restore_backup(bpy.types.Operator):
+ """Restore addon from backup"""
+ bl_label = "Restore backup"
+ bl_idname = updater.addon+".updater_restore_backup"
+ bl_description = "Restore addon from backup"
+ bl_options = {'REGISTER', 'INTERNAL'}
+
+ @classmethod
+ def poll(cls, context):
+ try:
+ return os.path.isdir(os.path.join(updater.stage_path,"backup"))
+ except:
+ return False
+
+ def execute(self, context):
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return {'CANCELLED'}
+ updater.restore_backup()
+ return {'FINISHED'}
+
+
+class addon_updater_ignore(bpy.types.Operator):
+ """Prevent future update notice popups"""
+ bl_label = "Ignore update"
+ bl_idname = updater.addon+".updater_ignore"
+ bl_description = "Ignore update to prevent future popups"
+ bl_options = {'REGISTER', 'INTERNAL'}
+
+ @classmethod
+ def poll(cls, context):
+ if updater.invalidupdater == True:
+ return False
+ elif updater.update_ready == True:
+ return True
+ else:
+ return False
+
+ def execute(self, context):
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return {'CANCELLED'}
+ updater.ignore_update()
+ self.report({"INFO"},"Open addon preferences for updater options")
+ return {'FINISHED'}
+
+
+class addon_updater_end_background(bpy.types.Operator):
+ """Stop checking for update in the background"""
+ bl_label = "End background check"
+ bl_idname = updater.addon+".end_background_check"
+ bl_description = "Stop checking for update in the background"
+ bl_options = {'REGISTER', 'INTERNAL'}
+
+ # @classmethod
+ # def poll(cls, context):
+ # if updater.async_checking == True:
+ # return True
+ # else:
+ # return False
+
+ def execute(self, context):
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return {'CANCELLED'}
+ updater.stop_async_check_update()
+ return {'FINISHED'}
+
+
+# -----------------------------------------------------------------------------
+# Handler related, to create popups
+# -----------------------------------------------------------------------------
+
+
+# global vars used to prevent duplicate popup handlers
+ran_autocheck_install_popup = False
+ran_update_sucess_popup = False
+
+# global var for preventing successive calls
+ran_background_check = False
+
+@persistent
+def updater_run_success_popup_handler(scene):
+ global ran_update_sucess_popup
+ ran_update_sucess_popup = True
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return
+
+ try:
+ bpy.app.handlers.scene_update_post.remove(
+ updater_run_success_popup_handler)
+ except:
+ pass
+
+ atr = addon_updater_updated_successful.bl_idname.split(".")
+ getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
+
+
+@persistent
+def updater_run_install_popup_handler(scene):
+ global ran_autocheck_install_popup
+ ran_autocheck_install_popup = True
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return
+
+ try:
+ bpy.app.handlers.scene_update_post.remove(
+ updater_run_install_popup_handler)
+ except:
+ pass
+
+ if "ignore" in updater.json and updater.json["ignore"] == True:
+ return # don't do popup if ignore pressed
+ # elif type(updater.update_version) != type((0,0,0)):
+ # # likely was from master or another branch, shouldn't trigger popup
+ # updater.json_reset_restore()
+ # return
+ elif "version_text" in updater.json and "version" in updater.json["version_text"]:
+ version = updater.json["version_text"]["version"]
+ ver_tuple = updater.version_tuple_from_text(version)
+
+ if ver_tuple < updater.current_version:
+ # user probably manually installed to get the up to date addon
+ # in here. Clear out the update flag using this function
+ if updater.verbose:
+ print("{} updater: appears user updated, clearing flag".format(\
+ updater.addon))
+ updater.json_reset_restore()
+ return
+ atr = addon_updater_install_popup.bl_idname.split(".")
+ getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
+
+
+# passed into the updater, background thread updater
+def background_update_callback(update_ready):
+ global ran_autocheck_install_popup
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return
+
+ if updater.showpopups == False:
+ return
+
+ if update_ready != True:
+ return
+
+ if updater_run_install_popup_handler not in \
+ bpy.app.handlers.scene_update_post and \
+ ran_autocheck_install_popup==False:
+ bpy.app.handlers.scene_update_post.append(
+ updater_run_install_popup_handler)
+
+ ran_autocheck_install_popup = True
+
+
+# a callback for once the updater has completed
+# Only makes sense to use this if "auto_reload_post_update" == False,
+# i.e. don't auto-restart the addon
+def post_update_callback(res=None):
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return
+
+ if res==None:
+ # this is the same code as in conditional at the end of the register function
+ # ie if "auto_reload_post_update" == True, comment out this code
+ if updater.verbose: print("{} updater: Running post update callback".format(updater.addon))
+ #bpy.app.handlers.scene_update_post.append(updater_run_success_popup_handler)
+
+ atr = addon_updater_updated_successful.bl_idname.split(".")
+ getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
+ global ran_update_sucess_popup
+ ran_update_sucess_popup = True
+ else:
+ # some kind of error occured and it was unable to install,
+ # offer manual download instead
+ atr = addon_updater_updated_successful.bl_idname.split(".")
+ getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT',error=res)
+ return
+
+def ui_refresh(update_status):
+ # find a way to just re-draw self?
+ # callback intended for trigger by async thread
+ for windowManager in bpy.data.window_managers:
+ for window in windowManager.windows:
+ for area in window.screen.areas:
+ area.tag_redraw()
+
+# function for asynchronous background check, which *could* be called on register
+def check_for_update_background():
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return
+
+ global ran_background_check
+ if ran_background_check == True:
+ # Global var ensures check only happens once
+ return
+ elif updater.update_ready != None or updater.async_checking == True:
+ # Check already happened
+ # Used here to just avoid constant applying settings below
+ return
+
+ # apply the UI settings
+ addon_prefs = bpy.context.user_preferences.addons.get(__package__, None)
+ if not addon_prefs:
+ return
+ settings = addon_prefs.preferences
+ updater.set_check_interval(enable=settings.auto_check_update,
+ months=settings.updater_intrval_months,
+ days=settings.updater_intrval_days,
+ hours=settings.updater_intrval_hours,
+ minutes=settings.updater_intrval_minutes
+ ) # optional, if auto_check_update
+
+ # input is an optional callback function
+ # this function should take a bool input, if true: update ready
+ # if false, no update ready
+ if updater.verbose:
+ print("{} updater: Running background check for update".format(\
+ updater.addon))
+ updater.check_for_update_async(background_update_callback)
+ ran_background_check = True
+
+
+# can be placed in front of other operators to launch when pressed
+def check_for_update_nonthreaded(self, context):
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return
+
+ # only check if it's ready, ie after the time interval specified
+ # should be the async wrapper call here
+
+ settings = context.user_preferences.addons[__package__].preferences
+ updater.set_check_interval(enable=settings.auto_check_update,
+ months=settings.updater_intrval_months,
+ days=settings.updater_intrval_days,
+ hours=settings.updater_intrval_hours,
+ minutes=settings.updater_intrval_minutes
+ ) # optional, if auto_check_update
+
+ (update_ready, version, link) = updater.check_for_update(now=False)
+ if update_ready == True:
+ atr = addon_updater_install_popup.bl_idname.split(".")
+ getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
+ else:
+ if updater.verbose: print("No update ready")
+ self.report({'INFO'}, "No update ready")
+
+# for use in register only, to show popup after re-enabling the addon
+# must be enabled by developer
+def showReloadPopup():
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return
+
+ saved_state = updater.json
+ global ran_update_sucess_popup
+
+ a = saved_state != None
+ b = "just_updated" in saved_state
+ c = saved_state["just_updated"]
+
+ if a and b and c:
+ updater.json_reset_postupdate() # so this only runs once
+
+ # no handlers in this case
+ if updater.auto_reload_post_update == False: return
+
+ if updater_run_success_popup_handler not in \
+ bpy.app.handlers.scene_update_post \
+ and ran_update_sucess_popup==False:
+ bpy.app.handlers.scene_update_post.append(
+ updater_run_success_popup_handler)
+ ran_update_sucess_popup = True
+
+
+# -----------------------------------------------------------------------------
+# Example UI integrations
+# -----------------------------------------------------------------------------
+
+
+# Panel - Update Available for placement at end/beginning of panel
+# After a check for update has occurred, this function will draw a box
+# saying an update is ready, and give a button for: update now, open website,
+# or ignore popup. Ideal to be placed at the end / beginning of a panel
+def update_notice_box_ui(self, context):
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ return
+
+ saved_state = updater.json
+ if updater.auto_reload_post_update == False:
+ if "just_updated" in saved_state and saved_state["just_updated"] == True:
+ layout = self.layout
+ box = layout.box()
+ col = box.column()
+ col.scale_y = 0.7
+ col.label("Restart blender", icon="ERROR")
+ col.label("to complete update")
+ return
+
+ # if user pressed ignore, don't draw the box
+ if "ignore" in updater.json and updater.json["ignore"] == True:
+ return
+
+ if updater.update_ready != True: return
+
+ settings = context.user_preferences.addons[__package__].preferences
+ layout = self.layout
+ box = layout.box()
+ col = box.column(align=True)
+ col.label("Update ready!",icon="ERROR")
+ col.separator()
+ row = col.row(align=True)
+ split = row.split(align=True)
+ colL = split.column(align=True)
+ colL.scale_y = 1.5
+ colL.operator(addon_updater_ignore.bl_idname,icon="X",text="Ignore")
+ colR = split.column(align=True)
+ colR.scale_y = 1.5
+ if updater.manual_only==False:
+ colR.operator(addon_updater_update_now.bl_idname,
+ "Update", icon="LOOP_FORWARDS")
+ col.operator("wm.url_open", text="Open website").url = updater.website
+ #col.operator("wm.url_open",text="Direct download").url=updater.update_link
+ col.operator(addon_updater_install_manually.bl_idname, "Install manually")
+ else:
+ #col.operator("wm.url_open",text="Direct download").url=updater.update_link
+ col.operator("wm.url_open", text="Get it now").url = \
+ updater.website
+
+
+# Preferences - for drawing with full width inside user preferences
+# Create a function that can be run inside user preferences panel for prefs UI
+# Place inside UI draw using: addon_updater_ops.updaterSettingsUI(self, context)
+# or by: addon_updater_ops.updaterSettingsUI(context)
+def update_settings_ui(self, context, element=None):
+ # element is a UI element, such as layout, a row, column, or box
+ if element==None: element = self.layout
+ box = element.box()
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ box.label("Error initializing updater code:")
+ box.label(updater.error_msg)
+ return
+
+ settings = context.user_preferences.addons[__package__].preferences
+
+ # auto-update settings
+ box.label("Updater Settings")
+ row = box.row()
+
+ # special case to tell user to restart blender, if set that way
+ if updater.auto_reload_post_update == False:
+ saved_state = updater.json
+ if "just_updated" in saved_state and saved_state["just_updated"] == True:
+ row.label("Restart blender to complete update", icon="ERROR")
+ return
+
+ split = row.split(percentage=0.3)
+ subcol = split.column()
+ subcol.prop(settings, "auto_check_update")
+ subcol = split.column()
+
+ if settings.auto_check_update==False: subcol.enabled = False
+ subrow = subcol.row()
+ subrow.label("Interval between checks")
+ subrow = subcol.row(align=True)
+ checkcol = subrow.column(align=True)
+ checkcol.prop(settings,"updater_intrval_months")
+ checkcol = subrow.column(align=True)
+ checkcol.prop(settings,"updater_intrval_days")
+ checkcol = subrow.column(align=True)
+ checkcol.prop(settings,"updater_intrval_hours")
+ checkcol = subrow.column(align=True)
+ checkcol.prop(settings,"updater_intrval_minutes")
+
+ # checking / managing updates
+ row = box.row()
+ col = row.column()
+ if updater.error != None:
+ subcol = col.row(align=True)
+ subcol.scale_y = 1
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ if "ssl" in updater.error_msg.lower():
+ split.enabled = True
+ split.operator(addon_updater_install_manually.bl_idname,
+ updater.error)
+ else:
+ split.enabled = False
+ split.operator(addon_updater_check_now.bl_idname,
+ updater.error)
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ text = "", icon="FILE_REFRESH")
+
+ elif updater.update_ready == None and updater.async_checking == False:
+ col.scale_y = 2
+ col.operator(addon_updater_check_now.bl_idname)
+ elif updater.update_ready == None: # async is running
+ subcol = col.row(align=True)
+ subcol.scale_y = 1
+ split = subcol.split(align=True)
+ split.enabled = False
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ "Checking...")
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_end_background.bl_idname,
+ text = "", icon="X")
+
+ elif updater.include_branches==True and \
+ len(updater.tags)==len(updater.include_branch_list) and \
+ updater.manual_only==False:
+ # no releases found, but still show the appropriate branch
+ subcol = col.row(align=True)
+ subcol.scale_y = 1
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_update_now.bl_idname,
+ "Update directly to "+str(updater.include_branch_list[0]))
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ text = "", icon="FILE_REFRESH")
+
+ elif updater.update_ready==True and updater.manual_only==False:
+ subcol = col.row(align=True)
+ subcol.scale_y = 1
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_update_now.bl_idname,
+ "Update now to "+str(updater.update_version))
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ text = "", icon="FILE_REFRESH")
+
+ elif updater.update_ready==True and updater.manual_only==True:
+ col.scale_y = 2
+ col.operator("wm.url_open",
+ "Download "+str(updater.update_version)).url=updater.website
+ else: # i.e. that updater.update_ready == False
+ subcol = col.row(align=True)
+ subcol.scale_y = 1
+ split = subcol.split(align=True)
+ split.enabled = False
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ "Addon is up to date")
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ text = "", icon="FILE_REFRESH")
+
+ if updater.manual_only == False:
+ col = row.column(align=True)
+ #col.operator(addon_updater_update_target.bl_idname,
+ if updater.include_branches == True and len(updater.include_branch_list)>0:
+ branch = updater.include_branch_list[0]
+ col.operator(addon_updater_update_target.bl_idname,
+ "Install latest {} / old version".format(branch))
+ else:
+ col.operator(addon_updater_update_target.bl_idname,
+ "Reinstall / install old version")
+ lastdate = "none found"
+ backuppath = os.path.join(updater.stage_path,"backup")
+ if "backup_date" in updater.json and os.path.isdir(backuppath):
+ if updater.json["backup_date"] == "":
+ lastdate = "Date not found"
+ else:
+ lastdate = updater.json["backup_date"]
+ backuptext = "Restore addon backup ({})".format(lastdate)
+ col.operator(addon_updater_restore_backup.bl_idname, backuptext)
+
+ row = box.row()
+ row.scale_y = 0.7
+ lastcheck = updater.json["last_check"]
+ if updater.error != None and updater.error_msg != None:
+ row.label(updater.error_msg)
+ elif lastcheck != "" and lastcheck != None:
+ lastcheck = lastcheck[0: lastcheck.index(".") ]
+ row.label("Last update check: " + lastcheck)
+ else:
+ row.label("Last update check: Never")
+
+
+# Preferences - Condensed drawing within preferences
+# alternate draw for user preferences or other places, does not draw a box
+def update_settings_ui_condensed(self, context, element=None):
+ # element is a UI element, such as layout, a row, column, or box
+ if element==None: element = self.layout
+ row = element.row()
+
+ # in case of error importing updater
+ if updater.invalidupdater == True:
+ row.label("Error initializing updater code:")
+ row.label(updater.error_msg)
+ return
+
+ settings = context.user_preferences.addons[__package__].preferences
+
+ # special case to tell user to restart blender, if set that way
+ if updater.auto_reload_post_update == False:
+ saved_state = updater.json
+ if "just_updated" in saved_state and saved_state["just_updated"] == True:
+ row.label("Restart blender to complete update", icon="ERROR")
+ return
+
+ col = row.column()
+ if updater.error != None:
+ subcol = col.row(align=True)
+ subcol.scale_y = 1
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ if "ssl" in updater.error_msg.lower():
+ split.enabled = True
+ split.operator(addon_updater_install_manually.bl_idname,
+ updater.error)
+ else:
+ split.enabled = False
+ split.operator(addon_updater_check_now.bl_idname,
+ updater.error)
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ text = "", icon="FILE_REFRESH")
+
+ elif updater.update_ready == None and updater.async_checking == False:
+ col.scale_y = 2
+ col.operator(addon_updater_check_now.bl_idname)
+ elif updater.update_ready == None: # async is running
+ subcol = col.row(align=True)
+ subcol.scale_y = 1
+ split = subcol.split(align=True)
+ split.enabled = False
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ "Checking...")
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_end_background.bl_idname,
+ text = "", icon="X")
+
+ elif updater.include_branches==True and \
+ len(updater.tags)==len(updater.include_branch_list) and \
+ updater.manual_only==False:
+ # no releases found, but still show the appropriate branch
+ subcol = col.row(align=True)
+ subcol.scale_y = 1
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_update_now.bl_idname,
+ "Update directly to "+str(updater.include_branch_list[0]))
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ text = "", icon="FILE_REFRESH")
+
+ elif updater.update_ready==True and updater.manual_only==False:
+ subcol = col.row(align=True)
+ subcol.scale_y = 1
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_update_now.bl_idname,
+ "Update now to "+str(updater.update_version))
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ text = "", icon="FILE_REFRESH")
+
+ elif updater.update_ready==True and updater.manual_only==True:
+ col.scale_y = 2
+ col.operator("wm.url_open",
+ "Download "+str(updater.update_version)).url=updater.website
+ else: # i.e. that updater.update_ready == False
+ subcol = col.row(align=True)
+ subcol.scale_y = 1
+ split = subcol.split(align=True)
+ split.enabled = False
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ "Addon is up to date")
+ split = subcol.split(align=True)
+ split.scale_y = 2
+ split.operator(addon_updater_check_now.bl_idname,
+ text = "", icon="FILE_REFRESH")
+
+ row = element.row()
+ row.prop(settings, "auto_check_update")
+
+ row = element.row()
+ row.scale_y = 0.7
+ lastcheck = updater.json["last_check"]
+ if updater.error != None and updater.error_msg != None:
+ row.label(updater.error_msg)
+ elif lastcheck != "" and lastcheck != None:
+ lastcheck = lastcheck[0: lastcheck.index(".") ]
+ row.label("Last check: " + lastcheck)
+ else:
+ row.label("Last check: Never")
+
+
+# a global function for tag skipping
+# a way to filter which tags are displayed,
+# e.g. to limit downgrading too far
+# input is a tag text, e.g. "v1.2.3"
+# output is True for skipping this tag number,
+# False if the tag is allowed (default for all)
+# Note: here, "self" is the acting updater shared class instance
+def skip_tag_function(self, tag):
+
+ # in case of error importing updater
+ if self.invalidupdater == True:
+ return False
+
+ # ---- write any custom code here, return true to disallow version ---- #
+ #
+ # # Filter out e.g. if 'beta' is in name of release
+ # if 'beta' in tag.lower():
+ # return True
+ # ---- write any custom code above, return true to disallow version --- #
+
+ if self.include_branches == True:
+ for branch in self.include_branch_list:
+ if tag["name"].lower() == branch: return False
+
+ # function converting string to tuple, ignoring e.g. leading 'v'
+ tupled = self.version_tuple_from_text(tag["name"])
+ if type(tupled) != type( (1,2,3) ): return True
+
+ # select the min tag version - change tuple accordingly
+ if self.version_min_update != None:
+ if tupled < self.version_min_update:
+ return True # skip if current version below this
+
+ # select the max tag version
+ if self.version_max_update != None:
+ if tupled >= self.version_max_update:
+ return True # skip if current version at or above this
+
+ # in all other cases, allow showing the tag for updating/reverting
+ return False
+
+# Only customize if trying to leverage "attachments" in *GitHub* releases
+# A way to select from one or multiple attached donwloadable files from the
+# server, instead of downloading the default release/tag source code
+def select_link_function(self, tag):
+ link = ""
+
+ # -- Default, universal case (and is the only option for GitLab/Bitbucket)
+ link = tag["zipball_url"]
+
+ # -- Example: select the first (or only) asset instead source code --
+ #if "assets" in tag and "browser_download_url" in tag["assets"][0]:
+ # link = tag["assets"][0]["browser_download_url"]
+
+ # -- Example: select asset based on OS, where multiple builds exist --
+ # # not tested/no error checking, modify to fit your own needs!
+ # # assume each release has three attached builds:
+ # # release_windows.zip, release_OSX.zip, release_linux.zip
+ # # This also would logically not be used with "branches" enabled
+ # if platform.system() == "Darwin": # ie OSX
+ # link = [asset for asset in tag["assets"] if 'OSX' in asset][0]
+ # elif platform.system() == "Windows":
+ # link = [asset for asset in tag["assets"] if 'windows' in asset][0]
+ # elif platform.system() == "Linux":
+ # link = [asset for asset in tag["assets"] if 'linux' in asset][0]
+
+ return link
+
+
+# -----------------------------------------------------------------------------
+# Register, should be run in the register module itself
+# -----------------------------------------------------------------------------
+
+
+# registering the operators in this module
+def register(bl_info):
+
+ # See output to verify this register function is working properly
+ # print("Running updater reg")
+
+ # safer failure in case of issue loading module
+ if updater.error != None:
+ print("Exiting updater registration, error return")
+ return
+
+ # confirm your updater "engine" (Github is default if not specified)
+ updater.engine = "Github"
+ # updater.engine = "GitLab"
+ # updater.engine = "Bitbucket"
+
+ # If using private repository, indicate the token here
+ # Must be set after assigning the engine.
+ # **WARNING** Depending on the engine, this token can act like a password!!
+ # Only provide a token if the project is *non-public*, see readme for
+ # other considerations and suggestions from a security standpoint
+ updater.private_token = None # "tokenstring"
+
+ # choose your own username, must match website (not needed for GitLab)
+ updater.user = "nutti"
+
+ # choose your own repository, must match git name
+ updater.repo = "Magic-UV"
+
+ #updater.addon = # define at top of module, MUST be done first
+
+ # Website for manual addon download, optional but recommended to set
+ updater.website = "https://github.com/nutti/Magic-UV"
+
+ # Addon subfolder path
+ # "sample/path/to/addon"
+ # default is "" or None, meaning root
+ updater.subfolder_path = "uv_magic_uv"
+
+ # used to check/compare versions
+ updater.current_version = bl_info["version"]
+
+ # Optional, to hard-set update frequency, use this here - however,
+ # this demo has this set via UI properties.
+ # updater.set_check_interval(
+ # enable=False,months=0,days=0,hours=0,minutes=2)
+
+ # Optional, consider turning off for production or allow as an option
+ # This will print out additional debugging info to the console
+ updater.verbose = False # make False for production default
+
+ # Optional, customize where the addon updater processing subfolder is,
+ # essentially a staging folder used by the updater on its own
+ # Needs to be within the same folder as the addon itself
+ # Need to supply a full, absolute path to folder
+ # updater.updater_path = # set path of updater folder, by default:
+ # /addons/{__package__}/{__package__}_updater
+
+ # auto create a backup of the addon when installing other versions
+ updater.backup_current = True # True by default
+
+ # Sample ignore patterns for when creating backup of current during update
+ updater.backup_ignore_patterns = ["__pycache__"]
+ # Alternate example patterns
+ # updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"]
+
+ # Patterns for files to actively overwrite if found in new update
+ # file and are also found in the currently installed addon. Note that
+
+ # by default (ie if set to []), updates are installed in the same way as blender:
+ # .py files are replaced, but other file types (e.g. json, txt, blend)
+ # will NOT be overwritten if already present in current install. Thus
+ # if you want to automatically update resources/non py files, add them
+ # as a part of the pattern list below so they will always be overwritten by an
+ # update. If a pattern file is not found in new update, no action is taken
+ # This does NOT detele anything, only defines what is allowed to be overwritten
+ updater.overwrite_patterns = ["*.png","*.jpg","README.md","LICENSE.txt"]
+ # updater.overwrite_patterns = []
+ # other examples:
+ # ["*"] means ALL files/folders will be overwritten by update, was the behavior pre updater v1.0.4
+ # [] or ["*.py","*.pyc"] matches default blender behavior, ie same effect if user installs update manually without deleting the existing addon first
+ # e.g. if existing install and update both have a resource.blend file, the existing installed one will remain
+ # ["some.py"] means if some.py is found in addon update, it will overwrite any existing some.py in current addon install, if any
+ # ["*.json"] means all json files found in addon update will overwrite those of same name in current install
+ # ["*.png","README.md","LICENSE.txt"] means the readme, license, and all pngs will be overwritten by update
+
+ # Patterns for files to actively remove prior to running update
+ # Useful if wanting to remove old code due to changes in filenames
+ # that otherwise would accumulate. Note: this runs after taking
+ # a backup (if enabled) but before placing in new update. If the same
+ # file name removed exists in the update, then it acts as if pattern
+ # is placed in the overwrite_patterns property. Note this is effectively
+ # ignored if clean=True in the run_update method
+ updater.remove_pre_update_patterns = ["*.py", "*.pyc"]
+ # Note setting ["*"] here is equivalent to always running updates with
+ # clean = True in the run_update method, ie the equivalent of a fresh,
+ # new install. This would also delete any resources or user-made/modified
+ # files setting ["__pycache__"] ensures the pycache folder is always removed
+ # The configuration of ["*.py","*.pyc"] is a safe option as this
+ # will ensure no old python files/caches remain in event different addon
+ # versions have different filenames or structures
+
+ # Allow branches like 'master' as an option to update to, regardless
+ # of release or version.
+ # Default behavior: releases will still be used for auto check (popup),
+ # but the user has the option from user preferences to directly
+ # update to the master branch or any other branches specified using
+ # the "install {branch}/older version" operator.
+ updater.include_branches = True
+
+ # (GitHub only) This options allows the user to use releases over tags for data,
+ # which enables pulling down release logs/notes, as well as specify installs from
+ # release-attached zips (instead of just the auto-packaged code generated with
+ # a release/tag). Setting has no impact on BitBucket or GitLab repos
+ updater.use_releases = False
+ # note: Releases always have a tag, but a tag may not always be a release
+ # Therefore, setting True above will filter out any non-annoted tags
+ # note 2: Using this option will also display the release name instead of
+ # just the tag name, bear this in mind given the skip_tag_function filtering above
+
+ # if using "include_branches",
+ # updater.include_branch_list defaults to ['master'] branch if set to none
+ # example targeting another multiple branches allowed to pull from
+ # updater.include_branch_list = ['master', 'dev'] # example with two branches
+ updater.include_branch_list = ['master', 'develop'] # None is the equivalent to setting ['master']
+
+ # Only allow manual install, thus prompting the user to open
+ # the addon's web page to download, specifically: updater.website
+ # Useful if only wanting to get notification of updates but not
+ # directly install.
+ updater.manual_only = False
+
+ # Used for development only, "pretend" to install an update to test
+ # reloading conditions
+ updater.fake_install = False # Set to true to test callback/reloading
+
+ # Show popups, ie if auto-check for update is enabled or a previous
+ # check for update in user preferences found a new version, show a popup
+ # (at most once per blender session, and it provides an option to ignore
+ # for future sessions); default behavior is set to True
+ updater.showpopups = True
+ # note: if set to false, there will still be an "update ready" box drawn
+ # using the `update_notice_box_ui` panel function.
+
+ # Override with a custom function on what tags
+ # to skip showing for updater; see code for function above.
+ # Set the min and max versions allowed to install.
+ # Optional, default None
+ # min install (>=) will install this and higher
+ updater.version_min_update = (5,2,0)
+ # updater.version_min_update = None # if not wanting to define a min
+
+ # max install (<) will install strictly anything lower
+ # updater.version_max_update = (9,9,9)
+ updater.version_max_update = None # if not wanting to define a max
+
+ # Function defined above, customize as appropriate per repository
+ updater.skip_tag = skip_tag_function # min and max used in this function
+
+ # Function defined above, customize as appropriate per repository; not required
+ updater.select_link = select_link_function
+
+ # The register line items for all operators/panels
+ # If using bpy.utils.register_module(__name__) to register elsewhere
+ # in the addon, delete these lines (also from unregister)
+ bpy.utils.register_class(addon_updater_install_popup)
+ bpy.utils.register_class(addon_updater_check_now)
+ bpy.utils.register_class(addon_updater_update_now)
+ bpy.utils.register_class(addon_updater_update_target)
+ bpy.utils.register_class(addon_updater_install_manually)
+ bpy.utils.register_class(addon_updater_updated_successful)
+ bpy.utils.register_class(addon_updater_restore_backup)
+ bpy.utils.register_class(addon_updater_ignore)
+ bpy.utils.register_class(addon_updater_end_background)
+
+ # special situation: we just updated the addon, show a popup
+ # to tell the user it worked
+ # should be enclosed in try/catch in case other issues arise
+ showReloadPopup()
+
+
+def unregister():
+ bpy.utils.unregister_class(addon_updater_install_popup)
+ bpy.utils.unregister_class(addon_updater_check_now)
+ bpy.utils.unregister_class(addon_updater_update_now)
+ bpy.utils.unregister_class(addon_updater_update_target)
+ bpy.utils.unregister_class(addon_updater_install_manually)
+ bpy.utils.unregister_class(addon_updater_updated_successful)
+ bpy.utils.unregister_class(addon_updater_restore_backup)
+ bpy.utils.unregister_class(addon_updater_ignore)
+ bpy.utils.unregister_class(addon_updater_end_background)
+
+ # clear global vars since they may persist if not restarting blender
+ updater.clear_state() # clear internal vars, avoids reloading oddities
+
+ global ran_autocheck_install_popup
+ ran_autocheck_install_popup = False
+
+ global ran_update_sucess_popup
+ ran_update_sucess_popup = False
+
+ global ran_background_check
+ ran_background_check = False
diff --git a/uv_magic_uv/common.py b/uv_magic_uv/common.py
index 475efd59..bad88167 100644
--- a/uv_magic_uv/common.py
+++ b/uv_magic_uv/common.py
@@ -20,19 +20,71 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
from collections import defaultdict
from pprint import pprint
from math import fabs, sqrt
+import os
import bpy
from mathutils import Vector
import bmesh
-DEBUG = False
+__all__ = [
+ 'is_console_mode',
+ 'is_debug_mode',
+ 'enable_debugg_mode',
+ 'disable_debug_mode',
+ 'debug_print',
+ 'check_version',
+ 'redraw_all_areas',
+ 'get_space',
+ 'mouse_on_region',
+ 'mouse_on_area',
+ 'mouse_on_regions',
+ 'create_bmesh',
+ 'create_new_uv_map',
+ 'get_island_info',
+ 'get_island_info_from_bmesh',
+ 'get_island_info_from_faces',
+ 'get_uvimg_editor_board_size',
+ 'calc_polygon_2d_area',
+ 'calc_polygon_3d_area',
+ 'measure_mesh_area',
+ 'measure_uv_area',
+ 'diff_point_to_segment',
+ 'get_loop_sequences',
+ 'get_overlapped_uv_info',
+ 'get_flipped_uv_info',
+]
+
+
+__DEBUG_MODE = True
+
+
+def is_console_mode():
+ if "MUV_CONSOLE_MODE" not in os.environ:
+ return False
+ return os.environ["MUV_CONSOLE_MODE"] == "True"
+
+
+def is_debug_mode():
+ return __DEBUG_MODE
+
+
+def enable_debugg_mode():
+ # pylint: disable=W0603
+ global __DEBUG_MODE
+ __DEBUG_MODE = True
+
+
+def disable_debug_mode():
+ # pylint: disable=W0603
+ global __DEBUG_MODE
+ __DEBUG_MODE = False
def debug_print(*s):
@@ -40,7 +92,7 @@ def debug_print(*s):
Print message to console in debugging mode
"""
- if DEBUG:
+ if is_debug_mode():
pprint(s)
@@ -91,6 +143,71 @@ def get_space(area_type, region_type, space_type):
return (area, region, space)
+def mouse_on_region(event, area_type, region_type):
+ pos = Vector((event.mouse_x, event.mouse_y))
+
+ _, region, _ = get_space(area_type, region_type, "")
+ if region is None:
+ return False
+
+ if (pos.x > region.x) and (pos.x < region.x + region.width) and \
+ (pos.y > region.y) and (pos.y < region.y + region.height):
+ return True
+
+ return False
+
+
+def mouse_on_area(event, area_type):
+ pos = Vector((event.mouse_x, event.mouse_y))
+
+ area, _, _ = get_space(area_type, "", "")
+ if area is None:
+ return False
+
+ if (pos.x > area.x) and (pos.x < area.x + area.width) and \
+ (pos.y > area.y) and (pos.y < area.y + area.height):
+ return True
+
+ return False
+
+
+def mouse_on_regions(event, area_type, regions):
+ if not mouse_on_area(event, area_type):
+ return False
+
+ for region in regions:
+ result = mouse_on_region(event, area_type, region)
+ if result:
+ return True
+
+ return False
+
+
+def create_bmesh(obj):
+ bm = bmesh.from_edit_mesh(obj.data)
+ if check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ return bm
+
+
+def create_new_uv_map(obj, name=None):
+ uv_maps_old = {l.name for l in obj.data.uv_layers}
+ bpy.ops.mesh.uv_texture_add()
+ uv_maps_new = {l.name for l in obj.data.uv_layers}
+ diff = uv_maps_new - uv_maps_old
+
+ if not list(diff):
+ return None # no more UV maps can not be created
+
+ # rename UV map
+ new = obj.data.uv_layers[list(diff)[0]]
+ if name:
+ new.name = name
+
+ return new
+
+
def __get_island_info(uv_layer, islands):
"""
get information about each island
@@ -273,7 +390,7 @@ def measure_mesh_area(obj):
return mesh_area
-def measure_uv_area(obj):
+def measure_uv_area(obj, tex_size=None):
bm = bmesh.from_edit_mesh(obj.data)
if check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
@@ -296,22 +413,38 @@ def measure_uv_area(obj):
uvs = [l[uv_layer].uv for l in f.loops]
f_uv_area = calc_polygon_2d_area(uvs)
- if not tex_layer:
- return None
- img = f[tex_layer].image
- # not found, try to search from node
+ # user specified
+ if tex_size:
+ uv_area = uv_area + f_uv_area * tex_size[0] * tex_size[1]
+ continue
+
+ # try to find from texture_layer
+ img = None
+ if tex_layer:
+ img = f[tex_layer].image
+
+ # not found, then try to search from node
if not img:
for mat in obj.material_slots:
+ if not mat.material.node_tree:
+ continue
for node in mat.material.node_tree.nodes:
tex_node_types = [
'TEX_ENVIRONMENT',
'TEX_IMAGE',
]
- if (node.type in tex_node_types) and node.image:
- img = node.image
+ if node.type not in tex_node_types:
+ continue
+ if not node.image:
+ continue
+ img = node.image
+
+ # can not find from node, so we can not get texture size
if not img:
return None
- uv_area = uv_area + f_uv_area * img.size[0] * img.size[1]
+
+ img_size = img.size
+ uv_area = uv_area + f_uv_area * img_size[0] * img_size[1]
return uv_area
@@ -602,3 +735,380 @@ def get_loop_sequences(bm, uv_layer, closed=False):
return None, err
return loop_seqs, ""
+
+
+def __is_segment_intersect(start1, end1, start2, end2):
+ seg1 = end1 - start1
+ seg2 = end2 - start2
+
+ a1 = -seg1.y
+ b1 = seg1.x
+ d1 = -(a1 * start1.x + b1 * start1.y)
+
+ a2 = -seg2.y
+ b2 = seg2.x
+ d2 = -(a2 * start2.x + b2 * start2.y)
+
+ seg1_line2_start = a2 * start1.x + b2 * start1.y + d2
+ seg1_line2_end = a2 * end1.x + b2 * end1.y + d2
+
+ seg2_line1_start = a1 * start2.x + b1 * start2.y + d1
+ seg2_line1_end = a1 * end2.x + b1 * end2.y + d1
+
+ if (seg1_line2_start * seg1_line2_end >= 0) or \
+ (seg2_line1_start * seg2_line1_end >= 0):
+ return False, None
+
+ u = seg1_line2_start / (seg1_line2_start - seg1_line2_end)
+ out = start1 + u * seg1
+
+ return True, out
+
+
+class RingBuffer:
+ def __init__(self, arr):
+ self.__buffer = arr.copy()
+ self.__pointer = 0
+
+ def __repr__(self):
+ return repr(self.__buffer)
+
+ def __len__(self):
+ return len(self.__buffer)
+
+ def insert(self, val, offset=0):
+ self.__buffer.insert(self.__pointer + offset, val)
+
+ def head(self):
+ return self.__buffer[0]
+
+ def tail(self):
+ return self.__buffer[-1]
+
+ def get(self, offset=0):
+ size = len(self.__buffer)
+ val = self.__buffer[(self.__pointer + offset) % size]
+ return val
+
+ def next(self):
+ size = len(self.__buffer)
+ self.__pointer = (self.__pointer + 1) % size
+
+ def reset(self):
+ self.__pointer = 0
+
+ def find(self, obj):
+ try:
+ idx = self.__buffer.index(obj)
+ except ValueError:
+ return None
+ return self.__buffer[idx]
+
+ def find_and_next(self, obj):
+ size = len(self.__buffer)
+ idx = self.__buffer.index(obj)
+ self.__pointer = (idx + 1) % size
+
+ def find_and_set(self, obj):
+ idx = self.__buffer.index(obj)
+ self.__pointer = idx
+
+ def as_list(self):
+ return self.__buffer.copy()
+
+ def reverse(self):
+ self.__buffer.reverse()
+ self.reset()
+
+
+# clip: reference polygon
+# subject: tested polygon
+def __do_weiler_atherton_cliping(clip, subject, uv_layer, mode):
+
+ clip_uvs = RingBuffer([l[uv_layer].uv.copy() for l in clip.loops])
+ if __is_polygon_flipped(clip_uvs):
+ clip_uvs.reverse()
+ subject_uvs = RingBuffer([l[uv_layer].uv.copy() for l in subject.loops])
+ if __is_polygon_flipped(subject_uvs):
+ subject_uvs.reverse()
+
+ debug_print("===== Clip UV List =====")
+ debug_print(clip_uvs)
+ debug_print("===== Subject UV List =====")
+ debug_print(subject_uvs)
+
+ # check if clip and subject is overlapped completely
+ if __is_polygon_same(clip_uvs, subject_uvs):
+ polygons = [subject_uvs.as_list()]
+ debug_print("===== Polygons Overlapped Completely =====")
+ debug_print(polygons)
+ return True, polygons
+
+ # check if subject is in clip
+ if __is_points_in_polygon(subject_uvs, clip_uvs):
+ polygons = [subject_uvs.as_list()]
+ return True, polygons
+
+ # check if clip is in subject
+ if __is_points_in_polygon(clip_uvs, subject_uvs):
+ polygons = [subject_uvs.as_list()]
+ return True, polygons
+
+ # check if clip and subject is overlapped partially
+ intersections = []
+ while True:
+ subject_uvs.reset()
+ while True:
+ uv_start1 = clip_uvs.get()
+ uv_end1 = clip_uvs.get(1)
+ uv_start2 = subject_uvs.get()
+ uv_end2 = subject_uvs.get(1)
+ intersected, point = __is_segment_intersect(uv_start1, uv_end1,
+ uv_start2, uv_end2)
+ if intersected:
+ clip_uvs.insert(point, 1)
+ subject_uvs.insert(point, 1)
+ intersections.append([point,
+ [clip_uvs.get(), clip_uvs.get(1)]])
+ subject_uvs.next()
+ if subject_uvs.get() == subject_uvs.head():
+ break
+ clip_uvs.next()
+ if clip_uvs.get() == clip_uvs.head():
+ break
+
+ debug_print("===== Intersection List =====")
+ debug_print(intersections)
+
+ # no intersection, so subject and clip is not overlapped
+ if not intersections:
+ return False, None
+
+ def get_intersection_pair(intersects, key):
+ for sect in intersects:
+ if sect[0] == key:
+ return sect[1]
+
+ return None
+
+ # make enter/exit pair
+ subject_uvs.reset()
+ subject_entering = []
+ subject_exiting = []
+ clip_entering = []
+ clip_exiting = []
+ intersect_uv_list = []
+ while True:
+ pair = get_intersection_pair(intersections, subject_uvs.get())
+ if pair:
+ sub = subject_uvs.get(1) - subject_uvs.get(-1)
+ inter = pair[1] - pair[0]
+ cross = sub.x * inter.y - inter.x * sub.y
+ if cross < 0:
+ subject_entering.append(subject_uvs.get())
+ clip_exiting.append(subject_uvs.get())
+ else:
+ subject_exiting.append(subject_uvs.get())
+ clip_entering.append(subject_uvs.get())
+ intersect_uv_list.append(subject_uvs.get())
+
+ subject_uvs.next()
+ if subject_uvs.get() == subject_uvs.head():
+ break
+
+ debug_print("===== Enter List =====")
+ debug_print(clip_entering)
+ debug_print(subject_entering)
+ debug_print("===== Exit List =====")
+ debug_print(clip_exiting)
+ debug_print(subject_exiting)
+
+ # for now, can't handle the situation when fulfill all below conditions
+ # * two faces have common edge
+ # * each face is intersected
+ # * Show Mode is "Part"
+ # so for now, ignore this situation
+ if len(subject_entering) != len(subject_exiting):
+ if mode == 'FACE':
+ polygons = [subject_uvs.as_list()]
+ return True, polygons
+ return False, None
+
+ def traverse(current_list, entering, exiting, p, current, other_list):
+ result = current_list.find(current)
+ if not result:
+ return None
+ if result != current:
+ print("Internal Error")
+ return None
+
+ # enter
+ if entering.count(current) >= 1:
+ entering.remove(current)
+
+ current_list.find_and_next(current)
+ current = current_list.get()
+
+ while exiting.count(current) == 0:
+ p.append(current.copy())
+ current_list.find_and_next(current)
+ current = current_list.get()
+
+ # exit
+ p.append(current.copy())
+ exiting.remove(current)
+
+ other_list.find_and_set(current)
+ return other_list.get()
+
+ # Traverse
+ polygons = []
+ current_uv_list = subject_uvs
+ other_uv_list = clip_uvs
+ current_entering = subject_entering
+ current_exiting = subject_exiting
+
+ poly = []
+ current_uv = current_entering[0]
+
+ while True:
+ current_uv = traverse(current_uv_list, current_entering,
+ current_exiting, poly, current_uv, other_uv_list)
+
+ if current_uv_list == subject_uvs:
+ current_uv_list = clip_uvs
+ other_uv_list = subject_uvs
+ current_entering = clip_entering
+ current_exiting = clip_exiting
+ debug_print("-- Next: Clip --")
+ else:
+ current_uv_list = subject_uvs
+ other_uv_list = clip_uvs
+ current_entering = subject_entering
+ current_exiting = subject_exiting
+ debug_print("-- Next: Subject --")
+
+ debug_print(clip_entering)
+ debug_print(clip_exiting)
+ debug_print(subject_entering)
+ debug_print(subject_exiting)
+
+ if not clip_entering and not clip_exiting \
+ and not subject_entering and not subject_exiting:
+ break
+
+ polygons.append(poly)
+
+ debug_print("===== Polygons Overlapped Partially =====")
+ debug_print(polygons)
+
+ return True, polygons
+
+
+def __is_polygon_flipped(points):
+ area = 0.0
+ for i in range(len(points)):
+ uv1 = points.get(i)
+ uv2 = points.get(i + 1)
+ a = uv1.x * uv2.y - uv1.y * uv2.x
+ area = area + a
+ if area < 0:
+ # clock-wise
+ return True
+ return False
+
+
+def __is_point_in_polygon(point, subject_points):
+ count = 0
+ for i in range(len(subject_points)):
+ uv_start1 = subject_points.get(i)
+ uv_end1 = subject_points.get(i + 1)
+ uv_start2 = point
+ uv_end2 = Vector((1000000.0, point.y))
+ intersected, _ = __is_segment_intersect(uv_start1, uv_end1,
+ uv_start2, uv_end2)
+ if intersected:
+ count = count + 1
+
+ return count % 2
+
+
+def __is_points_in_polygon(points, subject_points):
+ for i in range(len(points)):
+ internal = __is_point_in_polygon(points.get(i), subject_points)
+ if not internal:
+ return False
+
+ return True
+
+
+def get_overlapped_uv_info(bm, faces, uv_layer, mode):
+ # at first, check island overlapped
+ isl = get_island_info_from_faces(bm, faces, uv_layer)
+ overlapped_isl_pairs = []
+ for i, i1 in enumerate(isl):
+ for i2 in isl[i + 1:]:
+ if (i1["max"].x < i2["min"].x) or (i2["max"].x < i1["min"].x) or \
+ (i1["max"].y < i2["min"].y) or (i2["max"].y < i1["min"].y):
+ continue
+ overlapped_isl_pairs.append([i1, i2])
+
+ # next, check polygon overlapped
+ overlapped_uvs = []
+ for oip in overlapped_isl_pairs:
+ for clip in oip[0]["faces"]:
+ f_clip = clip["face"]
+ for subject in oip[1]["faces"]:
+ f_subject = subject["face"]
+
+ # fast operation, apply bounding box algorithm
+ if (clip["max_uv"].x < subject["min_uv"].x) or \
+ (subject["max_uv"].x < clip["min_uv"].x) or \
+ (clip["max_uv"].y < subject["min_uv"].y) or \
+ (subject["max_uv"].y < clip["min_uv"].y):
+ continue
+
+ # slow operation, apply Weiler-Atherton cliping algorithm
+ result, polygons = __do_weiler_atherton_cliping(f_clip,
+ f_subject,
+ uv_layer, mode)
+ if result:
+ subject_uvs = [l[uv_layer].uv.copy()
+ for l in f_subject.loops]
+ overlapped_uvs.append({"clip_face": f_clip,
+ "subject_face": f_subject,
+ "subject_uvs": subject_uvs,
+ "polygons": polygons})
+
+ return overlapped_uvs
+
+
+def get_flipped_uv_info(faces, uv_layer):
+ flipped_uvs = []
+ for f in faces:
+ polygon = RingBuffer([l[uv_layer].uv.copy() for l in f.loops])
+ if __is_polygon_flipped(polygon):
+ uvs = [l[uv_layer].uv.copy() for l in f.loops]
+ flipped_uvs.append({"face": f, "uvs": uvs,
+ "polygons": [polygon.as_list()]})
+
+ return flipped_uvs
+
+
+def __is_polygon_same(points1, points2):
+ if len(points1) != len(points2):
+ return False
+
+ pts1 = points1.as_list()
+ pts2 = points2.as_list()
+
+ for p1 in pts1:
+ for p2 in pts2:
+ diff = p2 - p1
+ if diff.length < 0.0000001:
+ pts2.remove(p2)
+ break
+ else:
+ return False
+
+ return True
diff --git a/uv_magic_uv/impl/__init__.py b/uv_magic_uv/impl/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/uv_magic_uv/impl/__init__.py
diff --git a/uv_magic_uv/impl/copy_paste_uv_impl.py b/uv_magic_uv/impl/copy_paste_uv_impl.py
new file mode 100644
index 00000000..ed44637b
--- /dev/null
+++ b/uv_magic_uv/impl/copy_paste_uv_impl.py
@@ -0,0 +1,271 @@
+# <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__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+
+
+import bmesh
+
+from .. import common
+
+
+__all__ = [
+ 'is_valid_context',
+ 'get_copy_uv_layers',
+ 'get_paste_uv_layers',
+ 'get_src_face_info',
+ 'get_dest_face_info',
+ 'get_select_history_src_face_info',
+ 'get_select_history_dest_face_info',
+ 'paste_uv',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def get_copy_uv_layers(ops_obj, bm, uv_map):
+ uv_layers = []
+ if uv_map == "__default":
+ if not bm.loops.layers.uv:
+ ops_obj.report(
+ {'WARNING'}, "Object must have more than one UV map")
+ return None
+ uv_layers.append(bm.loops.layers.uv.verify())
+ ops_obj.report({'INFO'}, "Copy UV coordinate")
+ elif uv_map == "__all":
+ for uv in bm.loops.layers.uv.keys():
+ uv_layers.append(bm.loops.layers.uv[uv])
+ ops_obj.report({'INFO'}, "Copy UV coordinate (UV map: ALL)")
+ else:
+ uv_layers.append(bm.loops.layers.uv[uv_map])
+ ops_obj.report(
+ {'INFO'}, "Copy UV coordinate (UV map:{})".format(uv_map))
+
+ return uv_layers
+
+
+def get_paste_uv_layers(ops_obj, obj, bm, src_info, uv_map):
+ uv_layers = []
+ if uv_map == "__default":
+ if not bm.loops.layers.uv:
+ ops_obj.report(
+ {'WARNING'}, "Object must have more than one UV map")
+ return None
+ uv_layers.append(bm.loops.layers.uv.verify())
+ ops_obj.report({'INFO'}, "Paste UV coordinate")
+ elif uv_map == "__new":
+ new_uv_map = common.create_new_uv_map(obj)
+ if not new_uv_map:
+ ops_obj.report({'WARNING'},
+ "Reached to the maximum number of UV map")
+ return None
+ uv_layers.append(bm.loops.layers.uv[new_uv_map.name])
+ ops_obj.report(
+ {'INFO'}, "Paste UV coordinate (UV map:{})".format(new_uv_map))
+ elif uv_map == "__all":
+ for src_layer in src_info.keys():
+ if src_layer not in bm.loops.layers.uv.keys():
+ new_uv_map = common.create_new_uv_map(obj, src_layer)
+ if not new_uv_map:
+ ops_obj.report({'WARNING'},
+ "Reached to the maximum number of UV map")
+ return None
+ uv_layers.append(bm.loops.layers.uv[src_layer])
+ ops_obj.report({'INFO'}, "Paste UV coordinate (UV map: ALL)")
+ else:
+ uv_layers.append(bm.loops.layers.uv[uv_map])
+ ops_obj.report(
+ {'INFO'}, "Paste UV coordinate (UV map:{})".format(uv_map))
+
+ return uv_layers
+
+
+def get_src_face_info(ops_obj, bm, uv_layers, only_select=False):
+ src_info = {}
+ for layer in uv_layers:
+ face_info = []
+ for face in bm.faces:
+ if not only_select or face.select:
+ info = {
+ "index": face.index,
+ "uvs": [l[layer].uv.copy() for l in face.loops],
+ "pin_uvs": [l[layer].pin_uv for l in face.loops],
+ "seams": [l.edge.seam for l in face.loops],
+ }
+ face_info.append(info)
+ if not face_info:
+ ops_obj.report({'WARNING'}, "No faces are selected")
+ return None
+ src_info[layer.name] = face_info
+
+ return src_info
+
+
+def get_dest_face_info(ops_obj, bm, uv_layers, src_info, strategy,
+ only_select=False):
+ dest_info = {}
+ for layer in uv_layers:
+ face_info = []
+ for face in bm.faces:
+ if not only_select or face.select:
+ info = {
+ "index": face.index,
+ "uvs": [l[layer].uv.copy() for l in face.loops],
+ }
+ face_info.append(info)
+ if not face_info:
+ ops_obj.report({'WARNING'}, "No faces are selected")
+ return None
+ key = list(src_info.keys())[0]
+ src_face_count = len(src_info[key])
+ dest_face_count = len(face_info)
+ if strategy == 'N_N' and src_face_count != dest_face_count:
+ ops_obj.report(
+ {'WARNING'},
+ "Number of selected faces is different from copied" +
+ "(src:{}, dest:{})"
+ .format(src_face_count, dest_face_count))
+ return None
+ dest_info[layer.name] = face_info
+
+ return dest_info
+
+
+def get_select_history_src_face_info(ops_obj, bm, uv_layers):
+ src_info = {}
+ for layer in uv_layers:
+ face_info = []
+ for hist in bm.select_history:
+ if isinstance(hist, bmesh.types.BMFace) and hist.select:
+ info = {
+ "index": hist.index,
+ "uvs": [l[layer].uv.copy() for l in hist.loops],
+ "pin_uvs": [l[layer].pin_uv for l in hist.loops],
+ "seams": [l.edge.seam for l in hist.loops],
+ }
+ face_info.append(info)
+ if not face_info:
+ ops_obj.report({'WARNING'}, "No faces are selected")
+ return None
+ src_info[layer.name] = face_info
+
+ return src_info
+
+
+def get_select_history_dest_face_info(ops_obj, bm, uv_layers, src_info,
+ strategy):
+ dest_info = {}
+ for layer in uv_layers:
+ face_info = []
+ for hist in bm.select_history:
+ if isinstance(hist, bmesh.types.BMFace) and hist.select:
+ info = {
+ "index": hist.index,
+ "uvs": [l[layer].uv.copy() for l in hist.loops],
+ }
+ face_info.append(info)
+ if not face_info:
+ ops_obj.report({'WARNING'}, "No faces are selected")
+ return None
+ key = list(src_info.keys())[0]
+ src_face_count = len(src_info[key])
+ dest_face_count = len(face_info)
+ if strategy == 'N_N' and src_face_count != dest_face_count:
+ ops_obj.report(
+ {'WARNING'},
+ "Number of selected faces is different from copied" +
+ "(src:{}, dest:{})"
+ .format(src_face_count, dest_face_count))
+ return None
+ dest_info[layer.name] = face_info
+
+ return dest_info
+
+
+def paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip,
+ rotate, copy_seams):
+ for slayer_name, dlayer in zip(src_info.keys(), uv_layers):
+ src_faces = src_info[slayer_name]
+ dest_faces = dest_info[dlayer.name]
+
+ for idx, dinfo in enumerate(dest_faces):
+ sinfo = None
+ if strategy == 'N_N':
+ sinfo = src_faces[idx]
+ elif strategy == 'N_M':
+ sinfo = src_faces[idx % len(src_faces)]
+
+ suv = sinfo["uvs"]
+ spuv = sinfo["pin_uvs"]
+ ss = sinfo["seams"]
+ if len(sinfo["uvs"]) != len(dinfo["uvs"]):
+ ops_obj.report({'WARNING'}, "Some faces are different size")
+ return -1
+
+ suvs_fr = [uv for uv in suv]
+ spuvs_fr = [pin_uv for pin_uv in spuv]
+ ss_fr = [s for s in ss]
+
+ # flip UVs
+ if flip is True:
+ suvs_fr.reverse()
+ spuvs_fr.reverse()
+ ss_fr.reverse()
+
+ # rotate UVs
+ for _ in range(rotate):
+ uv = suvs_fr.pop()
+ pin_uv = spuvs_fr.pop()
+ s = ss_fr.pop()
+ suvs_fr.insert(0, uv)
+ spuvs_fr.insert(0, pin_uv)
+ ss_fr.insert(0, s)
+
+ # paste UVs
+ for l, suv, spuv, ss in zip(bm.faces[dinfo["index"]].loops,
+ suvs_fr, spuvs_fr, ss_fr):
+ l[dlayer].uv = suv
+ l[dlayer].pin_uv = spuv
+ if copy_seams is True:
+ l.edge.seam = ss
+
+ return 0
diff --git a/uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py b/uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py
new file mode 100644
index 00000000..f14a70d6
--- /dev/null
+++ b/uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py
@@ -0,0 +1,166 @@
+# <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__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import math
+from math import atan2, sin, cos
+
+import bmesh
+from mathutils import Vector
+
+from .. import common
+
+
+__all__ = [
+ 'is_valid_context',
+ 'CopyUVImpl',
+ 'PasteUVImpl',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+class CopyUVImpl:
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
+ def execute(self, _, context):
+ props = context.scene.muv_props.copy_paste_uv_uvedit
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ uv_layer = bm.loops.layers.uv.verify()
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ props.src_uvs = []
+ for face in bm.faces:
+ if not face.select:
+ continue
+ skip = False
+ for l in face.loops:
+ if not l[uv_layer].select:
+ skip = True
+ break
+ if skip:
+ continue
+ props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops])
+
+ return {'FINISHED'}
+
+
+class PasteUVImpl:
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_uvedit
+ if not props.src_uvs:
+ return False
+ return is_valid_context(context)
+
+ def execute(self, _, context):
+ props = context.scene.muv_props.copy_paste_uv_uvedit
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ uv_layer = bm.loops.layers.uv.verify()
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ dest_uvs = []
+ dest_face_indices = []
+ for face in bm.faces:
+ if not face.select:
+ continue
+ skip = False
+ for l in face.loops:
+ if not l[uv_layer].select:
+ skip = True
+ break
+ if skip:
+ continue
+ dest_face_indices.append(face.index)
+ uvs = [l[uv_layer].uv.copy() for l in face.loops]
+ dest_uvs.append(uvs)
+
+ for suvs, duvs in zip(props.src_uvs, dest_uvs):
+ src_diff = suvs[1] - suvs[0]
+ dest_diff = duvs[1] - duvs[0]
+
+ src_base = suvs[0]
+ dest_base = duvs[0]
+
+ src_rad = atan2(src_diff.y, src_diff.x)
+ dest_rad = atan2(dest_diff.y, dest_diff.x)
+ if src_rad < dest_rad:
+ radian = dest_rad - src_rad
+ elif src_rad > dest_rad:
+ radian = math.pi * 2 - (src_rad - dest_rad)
+ else: # src_rad == dest_rad
+ radian = 0.0
+
+ ratio = dest_diff.length / src_diff.length
+ break
+
+ for suvs, fidx in zip(props.src_uvs, dest_face_indices):
+ for l, suv in zip(bm.faces[fidx].loops, suvs):
+ base = suv - src_base
+ radian_ref = atan2(base.y, base.x)
+ radian_fin = (radian + radian_ref)
+ length = base.length
+ turn = Vector((length * cos(radian_fin),
+ length * sin(radian_fin)))
+ target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \
+ dest_base
+ l[uv_layer].uv = target_uv
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/impl/flip_rotate_impl.py b/uv_magic_uv/impl/flip_rotate_impl.py
new file mode 100644
index 00000000..f74bc256
--- /dev/null
+++ b/uv_magic_uv/impl/flip_rotate_impl.py
@@ -0,0 +1,133 @@
+# <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__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+
+
+__all__ = [
+ 'is_valid_context',
+ 'get_uv_layer',
+ 'get_src_face_info',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def get_uv_layer(ops_obj, bm):
+ # get UV layer
+ if not bm.loops.layers.uv:
+ ops_obj.report({'WARNING'}, "Object must have more than one UV map")
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+
+ return uv_layer
+
+
+def get_src_face_info(ops_obj, bm, uv_layers, only_select=False):
+ src_info = {}
+ for layer in uv_layers:
+ face_info = []
+ for face in bm.faces:
+ if not only_select or face.select:
+ info = {
+ "index": face.index,
+ "uvs": [l[layer].uv.copy() for l in face.loops],
+ "pin_uvs": [l[layer].pin_uv for l in face.loops],
+ "seams": [l.edge.seam for l in face.loops],
+ }
+ face_info.append(info)
+ if not face_info:
+ ops_obj.report({'WARNING'}, "No faces are selected")
+ return None
+ src_info[layer.name] = face_info
+
+ return src_info
+
+
+def paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip,
+ rotate, copy_seams):
+ for slayer_name, dlayer in zip(src_info.keys(), uv_layers):
+ src_faces = src_info[slayer_name]
+ dest_faces = dest_info[dlayer.name]
+
+ for idx, dinfo in enumerate(dest_faces):
+ sinfo = None
+ if strategy == 'N_N':
+ sinfo = src_faces[idx]
+ elif strategy == 'N_M':
+ sinfo = src_faces[idx % len(src_faces)]
+
+ suv = sinfo["uvs"]
+ spuv = sinfo["pin_uvs"]
+ ss = sinfo["seams"]
+ if len(sinfo["uvs"]) != len(dinfo["uvs"]):
+ ops_obj.report({'WARNING'}, "Some faces are different size")
+ return -1
+
+ suvs_fr = [uv for uv in suv]
+ spuvs_fr = [pin_uv for pin_uv in spuv]
+ ss_fr = [s for s in ss]
+
+ # flip UVs
+ if flip is True:
+ suvs_fr.reverse()
+ spuvs_fr.reverse()
+ ss_fr.reverse()
+
+ # rotate UVs
+ for _ in range(rotate):
+ uv = suvs_fr.pop()
+ pin_uv = spuvs_fr.pop()
+ s = ss_fr.pop()
+ suvs_fr.insert(0, uv)
+ spuvs_fr.insert(0, pin_uv)
+ ss_fr.insert(0, s)
+
+ # paste UVs
+ for l, suv, spuv, ss in zip(bm.faces[dinfo["index"]].loops,
+ suvs_fr, spuvs_fr, ss_fr):
+ l[dlayer].uv = suv
+ l[dlayer].pin_uv = spuv
+ if copy_seams is True:
+ l.edge.seam = ss
+
+ return 0
diff --git a/uv_magic_uv/impl/mirror_uv_impl.py b/uv_magic_uv/impl/mirror_uv_impl.py
new file mode 100644
index 00000000..e79fbc2c
--- /dev/null
+++ b/uv_magic_uv/impl/mirror_uv_impl.py
@@ -0,0 +1,158 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bmesh
+from mathutils import Vector
+
+from .. import common
+
+
+__all__ = [
+ 'is_valid_context',
+ 'is_vector_similar',
+ 'mirror_uvs',
+ 'get_face_center',
+ 'MirrorUVImpl',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def is_vector_similar(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(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 is_vector_similar(svco, dvco, error):
+ dl[uv_layer].uv = suv.copy()
+
+
+def get_face_center(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)
+
+
+class MirrorUVImpl:
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
+ def execute(self, ops_obj, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+
+ error = ops_obj.error
+ axis = ops_obj.axis
+
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ if not bm.loops.layers.uv:
+ ops_obj.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 = get_face_center(f_dst)
+ src = 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 is_vector_similar(dst, src, error):
+ mirror_uvs(
+ uv_layer, f_src, f_dst, ops_obj.axis, ops_obj.error)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/impl/move_uv_impl.py b/uv_magic_uv/impl/move_uv_impl.py
new file mode 100644
index 00000000..ce507fba
--- /dev/null
+++ b/uv_magic_uv/impl/move_uv_impl.py
@@ -0,0 +1,166 @@
+# <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__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bmesh
+from mathutils import Vector
+
+from .. import common
+
+
+__all__ = [
+ 'is_valid_context',
+ 'find_uv',
+ 'MoveUVImpl',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def find_uv(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
+
+
+class MoveUVImpl():
+ __running = False
+
+ 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.__operating = False
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ if cls.is_running(context):
+ return False
+ return is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return cls.__running
+
+ 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 not self.__operating:
+ if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
+ self.__operating = True
+ 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
+ MoveUVImpl.__running = False
+ return {'FINISHED'}
+ # confirmed
+ if event.type == confirm_btn and event.value == 'PRESS':
+ MoveUVImpl.__running = False
+ return {'FINISHED'}
+
+ return {'RUNNING_MODAL'}
+
+ def execute(self, ops_obj, context):
+ MoveUVImpl.__running = True
+ self.__operating = False
+ self.__first_time = True
+
+ context.window_manager.modal_handler_add(ops_obj)
+ self.__topology_dict, self.__ini_uvs = find_uv(context)
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'RUNNING_MODAL'}
diff --git a/uv_magic_uv/impl/transfer_uv_impl.py b/uv_magic_uv/impl/transfer_uv_impl.py
new file mode 100644
index 00000000..adc97352
--- /dev/null
+++ b/uv_magic_uv/impl/transfer_uv_impl.py
@@ -0,0 +1,330 @@
+# <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__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from collections import OrderedDict
+
+import bpy
+import bmesh
+
+from .. import common
+
+
+__all__ = [
+ 'is_valid_context',
+ 'get_uv_layer',
+ 'main_parse',
+ 'parse_faces',
+ 'get_new_shared_faces',
+ 'get_other_verts_edges',
+ 'get_selected_src_faces',
+ 'paste_uv',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def get_uv_layer(ops_obj, bm):
+ # get UV layer
+ if not bm.loops.layers.uv:
+ ops_obj.report({'WARNING'}, "Object must have more than one UV map")
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+
+ return uv_layer
+
+
+def main_parse(ops_obj, 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:
+ ops_obj.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 is None:
+ ops_obj.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 None
+
+ 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]
+
+ 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]
+
+ 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 common.is_debug_mode():
+ 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]
+
+
+def get_selected_src_faces(ops_obj, bm, uv_layer):
+ topology_copied = []
+
+ # get selected faces
+ active_face = bm.faces.active
+ sel_faces = [face for face in bm.faces if face.select]
+ if len(sel_faces) != 2:
+ ops_obj.report({'WARNING'}, "Two faces must be selected")
+ return None
+ if not active_face or active_face not in sel_faces:
+ ops_obj.report({'WARNING'}, "Two faces must be active")
+ return None
+
+ # parse all faces according to selection
+ active_face_nor = active_face.normal.copy()
+ all_sorted_faces = main_parse(ops_obj, uv_layer, sel_faces, active_face,
+ active_face_nor)
+
+ if all_sorted_faces:
+ for face_data in all_sorted_faces.values():
+ edges = face_data[1]
+ uv_loops = face_data[2]
+ uvs = [l.uv.copy() for l in uv_loops]
+ pin_uvs = [l.pin_uv for l in uv_loops]
+ seams = [e.seam for e in edges]
+ topology_copied.append([uvs, pin_uvs, seams])
+ else:
+ return None
+
+ return topology_copied
+
+
+def paste_uv(ops_obj, bm, uv_layer, src_faces, invert_normals, copy_seams):
+ # 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:
+ ops_obj.report({'WARNING'}, "Two faces must be selected")
+ return -1
+
+ # parse selection history
+ for i, _ in enumerate(all_sel_faces):
+ if (i == 0) or (i % 2 == 0):
+ continue
+ 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 invert_normals:
+ active_face_nor.negate()
+ all_sorted_faces = main_parse(ops_obj, 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(src_faces):
+ ops_obj.report({'WARNING'},
+ "Mesh has different amount of faces")
+ return -1
+
+ for j, face_data in enumerate(all_sorted_faces.values()):
+ copied_data = src_faces[j]
+
+ # 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())[j].select = True
+ ops_obj.report({'WARNING'},
+ "Face have different amount of vertices")
+ return 0
+
+ for k, (edge, uvloop) in enumerate(zip(face_data[1],
+ face_data[2])):
+ uvloop.uv = copied_data[0][k]
+ uvloop.pin_uv = copied_data[1][k]
+ if copy_seams:
+ edge.seam = copied_data[2][k]
+ else:
+ return -1
+
+ return 0
diff --git a/uv_magic_uv/impl/uvw_impl.py b/uv_magic_uv/impl/uvw_impl.py
new file mode 100644
index 00000000..e815f54f
--- /dev/null
+++ b/uv_magic_uv/impl/uvw_impl.py
@@ -0,0 +1,154 @@
+# <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__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+
+
+from math import sin, cos, pi
+
+from mathutils import Vector
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def get_uv_layer(ops_obj, bm, assign_uvmap):
+ # get UV layer
+ if not bm.loops.layers.uv:
+ if assign_uvmap:
+ bm.loops.layers.uv.new()
+ else:
+ ops_obj.report({'WARNING'},
+ "Object must have more than one UV map")
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+
+ return uv_layer
+
+
+def apply_box_map(bm, uv_layer, size, offset, rotation, tex_aspect):
+ scale = 1.0 / size
+
+ sx = 1.0 * scale
+ sy = 1.0 * scale
+ sz = 1.0 * scale
+ ofx = offset[0]
+ ofy = offset[1]
+ ofz = offset[2]
+ rx = rotation[0] * pi / 180.0
+ ry = rotation[1] * pi / 180.0
+ rz = rotation[2] * pi / 180.0
+ aspect = tex_aspect
+
+ sel_faces = [f for f in bm.faces if f.select]
+
+ # update UV coordinate
+ for f in sel_faces:
+ n = f.normal
+ for l in f.loops:
+ co = l.vert.co
+ x = co.x * sx
+ y = co.y * sy
+ z = co.z * sz
+
+ # X-plane
+ if abs(n[0]) >= abs(n[1]) and abs(n[0]) >= abs(n[2]):
+ if n[0] >= 0.0:
+ u = (y - ofy) * cos(rx) + (z - ofz) * sin(rx)
+ v = -(y * aspect - ofy) * sin(rx) + \
+ (z * aspect - ofz) * cos(rx)
+ else:
+ u = -(y - ofy) * cos(rx) + (z - ofz) * sin(rx)
+ v = (y * aspect - ofy) * sin(rx) + \
+ (z * aspect - ofz) * cos(rx)
+ # Y-plane
+ elif abs(n[1]) >= abs(n[0]) and abs(n[1]) >= abs(n[2]):
+ if n[1] >= 0.0:
+ u = -(x - ofx) * cos(ry) + (z - ofz) * sin(ry)
+ v = (x * aspect - ofx) * sin(ry) + \
+ (z * aspect - ofz) * cos(ry)
+ else:
+ u = (x - ofx) * cos(ry) + (z - ofz) * sin(ry)
+ v = -(x * aspect - ofx) * sin(ry) + \
+ (z * aspect - ofz) * cos(ry)
+ # Z-plane
+ else:
+ if n[2] >= 0.0:
+ u = (x - ofx) * cos(rz) + (y - ofy) * sin(rz)
+ v = -(x * aspect - ofx) * sin(rz) + \
+ (y * aspect - ofy) * cos(rz)
+ else:
+ u = -(x - ofx) * cos(rz) - (y + ofy) * sin(rz)
+ v = -(x * aspect + ofx) * sin(rz) + \
+ (y * aspect - ofy) * cos(rz)
+
+ l[uv_layer].uv = Vector((u, v))
+
+
+def apply_planer_map(bm, uv_layer, size, offset, rotation, tex_aspect):
+ scale = 1.0 / size
+
+ sx = 1.0 * scale
+ sy = 1.0 * scale
+ ofx = offset[0]
+ ofy = offset[1]
+ rz = rotation * pi / 180.0
+ aspect = tex_aspect
+
+ sel_faces = [f for f in bm.faces if f.select]
+
+ # calculate average of normal
+ n_ave = Vector((0.0, 0.0, 0.0))
+ for f in sel_faces:
+ n_ave = n_ave + f.normal
+ q = n_ave.rotation_difference(Vector((0.0, 0.0, 1.0)))
+
+ # update UV coordinate
+ for f in sel_faces:
+ for l in f.loops:
+ co = q @ l.vert.co
+ x = co.x * sx
+ y = co.y * sy
+
+ u = x * cos(rz) - y * sin(rz) + ofx
+ v = -x * aspect * sin(rz) - y * aspect * cos(rz) + ofy
+
+ l[uv_layer].uv = Vector((u, v))
diff --git a/uv_magic_uv/legacy/__init__.py b/uv_magic_uv/legacy/__init__.py
new file mode 100644
index 00000000..794d02bc
--- /dev/null
+++ b/uv_magic_uv/legacy/__init__.py
@@ -0,0 +1,38 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(op)
+ importlib.reload(ui)
+ importlib.reload(properites)
+ importlib.reload(preferences)
+else:
+ from . import op
+ from . import ui
+ from . import properites
+ from . import preferences
+
+import bpy
diff --git a/uv_magic_uv/legacy/op/__init__.py b/uv_magic_uv/legacy/op/__init__.py
new file mode 100644
index 00000000..9535b76d
--- /dev/null
+++ b/uv_magic_uv/legacy/op/__init__.py
@@ -0,0 +1,74 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(align_uv)
+ importlib.reload(align_uv_cursor)
+ importlib.reload(copy_paste_uv)
+ importlib.reload(copy_paste_uv_object)
+ importlib.reload(copy_paste_uv_uvedit)
+ importlib.reload(flip_rotate_uv)
+ importlib.reload(mirror_uv)
+ importlib.reload(move_uv)
+ importlib.reload(pack_uv)
+ importlib.reload(preserve_uv_aspect)
+ importlib.reload(select_uv)
+ importlib.reload(smooth_uv)
+ importlib.reload(texture_lock)
+ importlib.reload(texture_projection)
+ importlib.reload(texture_wrap)
+ importlib.reload(transfer_uv)
+ importlib.reload(unwrap_constraint)
+ importlib.reload(uv_bounding_box)
+ importlib.reload(uv_inspection)
+ importlib.reload(uv_sculpt)
+ importlib.reload(uvw)
+ importlib.reload(world_scale_uv)
+else:
+ from . import align_uv
+ from . import align_uv_cursor
+ from . import copy_paste_uv
+ from . import copy_paste_uv_object
+ from . import copy_paste_uv_uvedit
+ from . import flip_rotate_uv
+ from . import mirror_uv
+ from . import move_uv
+ from . import pack_uv
+ from . import preserve_uv_aspect
+ from . import select_uv
+ from . import smooth_uv
+ from . import texture_lock
+ from . import texture_projection
+ from . import texture_wrap
+ from . import transfer_uv
+ from . import unwrap_constraint
+ from . import uv_bounding_box
+ from . import uv_inspection
+ from . import uv_sculpt
+ from . import uvw
+ from . import world_scale_uv
+
+import bpy
diff --git a/uv_magic_uv/op/align_uv.py b/uv_magic_uv/legacy/op/align_uv.py
index dcfb57c3..9d0ff5f4 100644
--- a/uv_magic_uv/op/align_uv.py
+++ b/uv_magic_uv/legacy/op/align_uv.py
@@ -20,8 +20,8 @@
__author__ = "imdjs, Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import math
from math import atan2, tan, sin, cos
@@ -29,9 +29,104 @@ from math import atan2, tan, sin, cos
import bpy
import bmesh
from mathutils import Vector
-from bpy.props import EnumProperty, BoolProperty
+from bpy.props import EnumProperty, BoolProperty, FloatProperty
-from .. import common
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_AlignUV_Circle',
+ 'MUV_OT_AlignUV_Straighten',
+ 'MUV_OT_AlignUV_Axis',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "align_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_align_uv_enabled = BoolProperty(
+ name="Align UV Enabled",
+ description="Align UV is enabled",
+ default=False
+ )
+ scene.muv_align_uv_transmission = BoolProperty(
+ name="Transmission",
+ description="Align linked UVs",
+ default=False
+ )
+ scene.muv_align_uv_select = BoolProperty(
+ name="Select",
+ description="Select UVs which are aligned",
+ default=False
+ )
+ scene.muv_align_uv_vertical = BoolProperty(
+ name="Vert-Infl (Vertical)",
+ description="Align vertical direction influenced "
+ "by mesh vertex proportion",
+ default=False
+ )
+ scene.muv_align_uv_horizontal = BoolProperty(
+ name="Vert-Infl (Horizontal)",
+ description="Align horizontal direction influenced "
+ "by mesh vertex proportion",
+ default=False
+ )
+ scene.muv_align_uv_mesh_infl = FloatProperty(
+ name="Mesh Influence",
+ description="Influence rate of mesh vertex",
+ min=0.0,
+ max=1.0,
+ default=0.0
+ )
+ scene.muv_align_uv_location = EnumProperty(
+ name="Location",
+ description="Align location",
+ items=[
+ ('LEFT_TOP', "Left/Top", "Align to Left or Top"),
+ ('MIDDLE', "Middle", "Align to middle"),
+ ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom")
+ ],
+ default='MIDDLE'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_align_uv_enabled
+ del scene.muv_align_uv_transmission
+ del scene.muv_align_uv_select
+ del scene.muv_align_uv_vertical
+ del scene.muv_align_uv_horizontal
+ del scene.muv_align_uv_mesh_infl
+ del scene.muv_align_uv_location
# get sum vertex length of loop sequences
@@ -86,10 +181,11 @@ def calc_v_on_circle(v, center, radius):
return new_v
-class MUV_AUVCircle(bpy.types.Operator):
+@BlClassRegistry(legacy=True)
+class MUV_OT_AlignUV_Circle(bpy.types.Operator):
- bl_idname = "uv.muv_auv_circle"
- bl_label = "Circle"
+ bl_idname = "uv.muv_align_uv_operator_circle"
+ bl_label = "Align UV (Circle)"
bl_description = "Align UV coordinates to Circle"
bl_options = {'REGISTER', 'UNDO'}
@@ -106,7 +202,10 @@ class MUV_AUVCircle(bpy.types.Operator):
@classmethod
def poll(cls, context):
- return context.mode == 'EDIT_MESH'
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
def execute(self, context):
obj = context.active_object
@@ -167,66 +266,164 @@ class MUV_AUVCircle(bpy.types.Operator):
return {'FINISHED'}
+# get accumulate vertex lengths of loop sequences
+def get_loop_vert_accum_len(loops):
+ accum_lengths = [0.0]
+ length = 0
+ for l1, l2 in zip(loops[:-1], loops[1:]):
+ diff = l2.vert.co - l1.vert.co
+ length = length + abs(diff.length)
+ accum_lengths.extend([length])
+
+ return accum_lengths
+
+
+# get sum uv length of loop sequences
+def get_loop_uv_accum_len(loops, uv_layer):
+ accum_lengths = [0.0]
+ length = 0
+ for l1, l2 in zip(loops[:-1], loops[1:]):
+ diff = l2[uv_layer].uv - l1[uv_layer].uv
+ length = length + abs(diff.length)
+ accum_lengths.extend([length])
+
+ return accum_lengths
+
+
# get horizontal differential of UV influenced by mesh vertex
-def get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pair_idx):
+def get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl):
common.debug_print(
- "vidx={0}, hidx={1}, pair_idx={2}".format(vidx, hidx, pair_idx))
+ "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx))
- # get total vertex length
- hloops = []
- for s in loop_seqs:
- hloops.extend([s[vidx][0], s[vidx][1]])
- vert_total_hlen = get_loop_vert_len(hloops)
- common.debug_print(vert_total_hlen)
+ base_uv = loop_seqs[0][vidx][0][uv_layer].uv.copy()
- # target vertex length
+ # calculate original length
hloops = []
- for s in loop_seqs[:hidx]:
+ for s in loop_seqs:
hloops.extend([s[vidx][0], s[vidx][1]])
- for pidx, l in enumerate(loop_seqs[hidx][vidx]):
- if pidx > pair_idx:
+ total_vlen = get_loop_vert_len(hloops)
+ accum_vlens = get_loop_vert_accum_len(hloops)
+ total_uvlen = get_loop_uv_len(hloops, uv_layer)
+ accum_uvlens = get_loop_uv_accum_len(hloops, uv_layer)
+ orig_uvs = [l[uv_layer].uv.copy() for l in hloops]
+
+ # calculate target length
+ tgt_noinfl = total_uvlen * (hidx + pidx) / len(loop_seqs)
+ tgt_infl = total_uvlen * accum_vlens[hidx * 2 + pidx] / total_vlen
+ target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl
+ common.debug_print(target_length)
+ common.debug_print(accum_uvlens)
+
+ # calculate target UV
+ for i in range(len(accum_uvlens[:-1])):
+ # get line segment which UV will be placed
+ if ((accum_uvlens[i] <= target_length) and
+ (accum_uvlens[i + 1] > target_length)):
+ tgt_seg_len = target_length - accum_uvlens[i]
+ seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
+ uv1 = orig_uvs[i]
+ uv2 = orig_uvs[i + 1]
+ target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
break
- hloops.append(l)
- vert_hlen = get_loop_vert_len(hloops)
- common.debug_print(vert_hlen)
+ elif i == (len(accum_uvlens[:-1]) - 1):
+ if accum_uvlens[i + 1] != target_length:
+ raise Exception(
+ "Internal Error: horizontal_target_length={}"
+ " is not equal to {}"
+ .format(target_length, accum_uvlens[-1]))
+ tgt_seg_len = target_length - accum_uvlens[i]
+ seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
+ uv1 = orig_uvs[i]
+ uv2 = orig_uvs[i + 1]
+ target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
+ break
+ else:
+ raise Exception("Internal Error: horizontal_target_length={}"
+ " is not in range {} to {}"
+ .format(target_length, accum_uvlens[0],
+ accum_uvlens[-1]))
+
+ return target_uv
- # get total UV length
- # uv_all_hdiff = loop_seqs[-1][0][-1][uv_layer].uv -
- # loop_seqs[0][0][0][uv_layer].uv
- uv_total_hlen = loop_seqs[-1][vidx][-1][uv_layer].uv -\
- loop_seqs[0][vidx][0][uv_layer].uv
- common.debug_print(uv_total_hlen)
- return uv_total_hlen * vert_hlen / vert_total_hlen
+# --------------------- LOOP STRUCTURE ----------------------
+#
+# loops[hidx][vidx][pidx]
+# hidx: horizontal index
+# vidx: vertical index
+# pidx: pair index
+#
+# <----- horizontal ----->
+#
+# (hidx, vidx, pidx) = (0, 3, 0)
+# | (hidx, vidx, pidx) = (1, 3, 0)
+# v v
+# ^ o --- oo --- o
+# | | || |
+# vertical | o --- oo --- o <- (hidx, vidx, pidx)
+# | o --- oo --- o = (1, 2, 1)
+# | | || |
+# v o --- oo --- o
+# ^ ^
+# | (hidx, vidx, pidx) = (1, 0, 1)
+# (hidx, vidx, pidx) = (0, 0, 0)
+#
+# -----------------------------------------------------------
# get vertical differential of UV influenced by mesh vertex
-def get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pair_idx):
+def get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl):
common.debug_print(
- "vidx={0}, hidx={1}, pair_idx={2}".format(vidx, hidx, pair_idx))
-
- # get total vertex length
- hloops = []
- for s in loop_seqs[hidx]:
- hloops.append(s[pair_idx])
- vert_total_hlen = get_loop_vert_len(hloops)
- common.debug_print(vert_total_hlen)
+ "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx))
- # target vertex length
- hloops = []
- for s in loop_seqs[hidx][:vidx + 1]:
- hloops.append(s[pair_idx])
- vert_hlen = get_loop_vert_len(hloops)
- common.debug_print(vert_hlen)
+ base_uv = loop_seqs[hidx][0][pidx][uv_layer].uv.copy()
- # get total UV length
- # uv_all_hdiff = loop_seqs[0][-1][pair_idx][uv_layer].uv - \
- # loop_seqs[0][0][pair_idx][uv_layer].uv
- uv_total_hlen = loop_seqs[hidx][-1][pair_idx][uv_layer].uv -\
- loop_seqs[hidx][0][pair_idx][uv_layer].uv
- common.debug_print(uv_total_hlen)
+ # calculate original length
+ vloops = []
+ for s in loop_seqs[hidx]:
+ vloops.append(s[pidx])
+ total_vlen = get_loop_vert_len(vloops)
+ accum_vlens = get_loop_vert_accum_len(vloops)
+ total_uvlen = get_loop_uv_len(vloops, uv_layer)
+ accum_uvlens = get_loop_uv_accum_len(vloops, uv_layer)
+ orig_uvs = [l[uv_layer].uv.copy() for l in vloops]
+
+ # calculate target length
+ tgt_noinfl = total_uvlen * int((vidx + 1) / 2) / len(loop_seqs)
+ tgt_infl = total_uvlen * accum_vlens[vidx] / total_vlen
+ target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl
+ common.debug_print(target_length)
+ common.debug_print(accum_uvlens)
+
+ # calculate target UV
+ for i in range(len(accum_uvlens[:-1])):
+ # get line segment which UV will be placed
+ if ((accum_uvlens[i] <= target_length) and
+ (accum_uvlens[i + 1] > target_length)):
+ tgt_seg_len = target_length - accum_uvlens[i]
+ seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
+ uv1 = orig_uvs[i]
+ uv2 = orig_uvs[i + 1]
+ target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
+ break
+ elif i == (len(accum_uvlens[:-1]) - 1):
+ if accum_uvlens[i + 1] != target_length:
+ raise Exception("Internal Error: horizontal_target_length={}"
+ " is not equal to {}"
+ .format(target_length, accum_uvlens[-1]))
+ tgt_seg_len = target_length - accum_uvlens[i]
+ seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
+ uv1 = orig_uvs[i]
+ uv2 = orig_uvs[i + 1]
+ target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
+ break
+ else:
+ raise Exception("Internal Error: horizontal_target_length={}"
+ " is not in range {} to {}"
+ .format(target_length, accum_uvlens[0],
+ accum_uvlens[-1]))
- return uv_total_hlen * vert_hlen / vert_total_hlen
+ return target_uv
# get horizontal differential of UV no influenced
@@ -246,10 +443,11 @@ def get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx):
return int((vidx + 1) / 2) * v_uv / (len(hseq) / 2)
-class MUV_AUVStraighten(bpy.types.Operator):
+@BlClassRegistry(legacy=True)
+class MUV_OT_AlignUV_Straighten(bpy.types.Operator):
- bl_idname = "uv.muv_auv_straighten"
- bl_label = "Straighten"
+ bl_idname = "uv.muv_align_uv_operator_straighten"
+ bl_label = "Align UV (Straighten)"
bl_description = "Straighten UV coordinates"
bl_options = {'REGISTER', 'UNDO'}
@@ -275,10 +473,20 @@ class MUV_AUVStraighten(bpy.types.Operator):
"by mesh vertex proportion",
default=False
)
+ mesh_infl = FloatProperty(
+ name="Mesh Influence",
+ description="Influence rate of mesh vertex",
+ min=0.0,
+ max=1.0,
+ default=0.0
+ )
@classmethod
def poll(cls, context):
- return context.mode == 'EDIT_MESH'
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
# selected and paralleled UV loop sequence will be aligned
def __align_w_transmission(self, loop_seqs, uv_layer):
@@ -293,12 +501,14 @@ class MUV_AUVStraighten(bpy.types.Operator):
for vidx in range(0, len(hseq), 2):
if self.horizontal:
hdiff_uvs = [
- get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0),
- get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1),
+ get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 0),
+ hidx, 0, self.mesh_infl),
get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 1),
+ hidx, 1, self.mesh_infl),
]
else:
hdiff_uvs = [
@@ -309,12 +519,14 @@ class MUV_AUVStraighten(bpy.types.Operator):
]
if self.vertical:
vdiff_uvs = [
- get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0),
- get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1),
+ get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 0),
+ hidx, 0, self.mesh_infl),
get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 1),
+ hidx, 1, self.mesh_infl),
]
else:
vdiff_uvs = [
@@ -382,10 +594,11 @@ class MUV_AUVStraighten(bpy.types.Operator):
return {'FINISHED'}
-class MUV_AUVAxis(bpy.types.Operator):
+@BlClassRegistry(legacy=True)
+class MUV_OT_AlignUV_Axis(bpy.types.Operator):
- bl_idname = "uv.muv_auv_axis"
- bl_label = "XY-Axis"
+ bl_idname = "uv.muv_align_uv_operator_axis"
+ bl_label = "Align UV (XY-Axis)"
bl_description = "Align UV to XY-axis"
bl_options = {'REGISTER', 'UNDO'}
@@ -421,10 +634,20 @@ class MUV_AUVAxis(bpy.types.Operator):
],
default='MIDDLE'
)
+ mesh_infl = FloatProperty(
+ name="Mesh Influence",
+ description="Influence rate of mesh vertex",
+ min=0.0,
+ max=1.0,
+ default=0.0
+ )
@classmethod
def poll(cls, context):
- return context.mode == 'EDIT_MESH'
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
# get min/max of UV
def __get_uv_max_min(self, loop_seqs, uv_layer):
@@ -579,12 +802,14 @@ class MUV_AUVAxis(bpy.types.Operator):
for vidx in range(0, len(hseq), 2):
if self.horizontal:
hdiff_uvs = [
- get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0),
- get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1),
+ get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 0),
+ hidx, 0, self.mesh_infl),
get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 1),
+ hidx, 1, self.mesh_infl),
]
hdiff_uvs[0].y = hdiff_uvs[0].y + offset_uvs[hidx][0].y
hdiff_uvs[1].y = hdiff_uvs[1].y + offset_uvs[hidx][1].y
@@ -599,12 +824,14 @@ class MUV_AUVAxis(bpy.types.Operator):
]
if self.vertical:
vdiff_uvs = [
- get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0),
- get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1),
+ get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 0),
+ hidx, 0, self.mesh_infl),
get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 1),
+ hidx, 1, self.mesh_infl),
]
else:
vdiff_uvs = [
@@ -664,12 +891,14 @@ class MUV_AUVAxis(bpy.types.Operator):
for vidx in range(0, len(hseq), 2):
if self.horizontal:
hdiff_uvs = [
- get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0),
- get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1),
+ get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 0),
+ hidx, 0, self.mesh_infl),
get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 1),
+ hidx, 1, self.mesh_infl),
]
hdiff_uvs[0].x = hdiff_uvs[0].x + offset_uvs[hidx][0].x
hdiff_uvs[1].x = hdiff_uvs[1].x + offset_uvs[hidx][1].x
@@ -684,12 +913,14 @@ class MUV_AUVAxis(bpy.types.Operator):
]
if self.vertical:
vdiff_uvs = [
- get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0),
- get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1),
+ get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 0),
+ hidx, 0, self.mesh_infl),
get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
- hidx, 1),
+ hidx, 1, self.mesh_infl),
]
else:
vdiff_uvs = [
diff --git a/uv_magic_uv/op/align_uv_cursor.py b/uv_magic_uv/legacy/op/align_uv_cursor.py
index cae1c89a..ec3e7036 100644
--- a/uv_magic_uv/op/align_uv_cursor.py
+++ b/uv_magic_uv/legacy/op/align_uv_cursor.py
@@ -20,21 +20,117 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
from mathutils import Vector
-from bpy.props import EnumProperty
+from bpy.props import EnumProperty, BoolProperty, FloatVectorProperty
import bmesh
-from .. import common
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
-class MUV_AUVCAlignOps(bpy.types.Operator):
+__all__ = [
+ 'Properties',
+ 'MUV_OT_AlignUVCursor',
+]
- bl_idname = "uv.muv_auvc_align"
- bl_label = "Align"
+
+def is_valid_context(context):
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "align_uv_cursor"
+
+ @classmethod
+ def init_props(cls, scene):
+ def auvc_get_cursor_loc(self):
+ area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
+ bd_size = common.get_uvimg_editor_board_size(area)
+ loc = space.cursor_location
+ if bd_size[0] < 0.000001:
+ cx = 0.0
+ else:
+ cx = loc[0] / bd_size[0]
+ if bd_size[1] < 0.000001:
+ cy = 0.0
+ else:
+ cy = loc[1] / bd_size[1]
+ self['muv_align_uv_cursor_cursor_loc'] = Vector((cx, cy))
+ return self.get('muv_align_uv_cursor_cursor_loc', (0.0, 0.0))
+
+ def auvc_set_cursor_loc(self, value):
+ self['muv_align_uv_cursor_cursor_loc'] = value
+ area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
+ bd_size = common.get_uvimg_editor_board_size(area)
+ cx = bd_size[0] * value[0]
+ cy = bd_size[1] * value[1]
+ space.cursor_location = Vector((cx, cy))
+
+ scene.muv_align_uv_cursor_enabled = BoolProperty(
+ name="Align UV Cursor Enabled",
+ description="Align UV Cursor is enabled",
+ default=False
+ )
+
+ scene.muv_align_uv_cursor_cursor_loc = FloatVectorProperty(
+ name="UV Cursor Location",
+ size=2,
+ precision=4,
+ soft_min=-1.0,
+ soft_max=1.0,
+ step=1,
+ default=(0.000, 0.000),
+ get=auvc_get_cursor_loc,
+ set=auvc_set_cursor_loc
+ )
+ scene.muv_align_uv_cursor_align_method = EnumProperty(
+ name="Align Method",
+ description="Align Method",
+ default='TEXTURE',
+ items=[
+ ('TEXTURE', "Texture", "Align to texture"),
+ ('UV', "UV", "Align to UV"),
+ ('UV_SEL', "UV (Selected)", "Align to Selected UV")
+ ]
+ )
+
+ scene.muv_uv_cursor_location_enabled = BoolProperty(
+ name="UV Cursor Location Enabled",
+ description="UV Cursor Location is enabled",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_align_uv_cursor_enabled
+ del scene.muv_align_uv_cursor_cursor_loc
+ del scene.muv_align_uv_cursor_align_method
+
+ del scene.muv_uv_cursor_location_enabled
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_AlignUVCursor(bpy.types.Operator):
+
+ bl_idname = "uv.muv_align_uv_cursor_operator"
+ bl_label = "Align UV Cursor"
bl_description = "Align cursor to the center of UV island"
bl_options = {'REGISTER', 'UNDO'}
@@ -65,6 +161,13 @@ class MUV_AUVCAlignOps(bpy.types.Operator):
default='TEXTURE'
)
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
def execute(self, context):
area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
'IMAGE_EDITOR')
diff --git a/uv_magic_uv/legacy/op/copy_paste_uv.py b/uv_magic_uv/legacy/op/copy_paste_uv.py
new file mode 100644
index 00000000..a8aef017
--- /dev/null
+++ b/uv_magic_uv/legacy/op/copy_paste_uv.py
@@ -0,0 +1,531 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+
+import bmesh
+import bpy.utils
+from bpy.props import (
+ StringProperty,
+ BoolProperty,
+ IntProperty,
+ EnumProperty,
+)
+
+from ...impl import copy_paste_uv_impl as impl
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_CopyPasteUV_CopyUV',
+ 'MUV_MT_CopyPasteUV_CopyUV',
+ 'MUV_OT_CopyPasteUV_PasteUV',
+ 'MUV_MT_CopyPasteUV_PasteUV',
+ 'MUV_OT_CopyPasteUV_SelSeqCopyUV',
+ 'MUV_MT_CopyPasteUV_SelSeqCopyUV',
+ 'MUV_OT_CopyPasteUV_SelSeqPasteUV',
+ 'MUV_MT_CopyPasteUV_SelSeqPasteUV',
+]
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "copy_paste_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ src_info = None
+
+ scene.muv_props.copy_paste_uv = Props()
+ scene.muv_props.copy_paste_uv_selseq = Props()
+
+ scene.muv_copy_paste_uv_enabled = BoolProperty(
+ name="Copy/Paste UV Enabled",
+ description="Copy/Paste UV is enabled",
+ default=False
+ )
+ scene.muv_copy_paste_uv_copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+ scene.muv_copy_paste_uv_mode = EnumProperty(
+ items=[
+ ('DEFAULT', "Default", "Default Mode"),
+ ('SEL_SEQ', "Selection Sequence", "Selection Sequence Mode")
+ ],
+ name="Copy/Paste UV Mode",
+ description="Copy/Paste UV Mode",
+ default='DEFAULT'
+ )
+ scene.muv_copy_paste_uv_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'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.copy_paste_uv
+ del scene.muv_props.copy_paste_uv_selseq
+ del scene.muv_copy_paste_uv_enabled
+ del scene.muv_copy_paste_uv_copy_seams
+ del scene.muv_copy_paste_uv_mode
+ del scene.muv_copy_paste_uv_strategy
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_CopyPasteUV_CopyUV(bpy.types.Operator):
+ """
+ Operation class: Copy UV coordinate
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_operator_copy_uv"
+ bl_label = "Copy UV"
+ bl_description = "Copy UV coordinate"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", options={'HIDDEN'})
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ src_info = impl.get_src_face_info(self, bm, uv_layers, True)
+ if src_info is None:
+ return {'CANCELLED'}
+ props.src_info = src_info
+
+ face_count = len(props.src_info[list(props.src_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are copied".format(face_count))
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_CopyPasteUV_CopyUV(bpy.types.Menu):
+ """
+ Menu class: Copy UV coordinate
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_menu_copy_uv"
+ bl_label = "Copy UV (Menu)"
+ bl_description = "Menu of Copy UV coordinate"
+
+ @classmethod
+ def poll(cls, context):
+ return impl.is_valid_context(context)
+
+ def draw(self, context):
+ layout = self.layout
+ # create sub menu
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+ uv_maps = bm.loops.layers.uv.keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, text=m)
+ ops.uv_map = m
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_CopyPasteUV_PasteUV(bpy.types.Operator):
+ """
+ Operation class: Paste UV coordinate
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_operator_paste_uv"
+ bl_label = "Paste UV"
+ bl_description = "Paste UV coordinate"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", 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
+ )
+ copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv
+ if not props.src_info:
+ return False
+ return impl.is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv
+ if not props.src_info:
+ self.report({'WARNING'}, "Need copy UV at first")
+ return {'CANCELLED'}
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info,
+ self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ dest_info = impl.get_dest_face_info(self, bm, uv_layers,
+ props.src_info, self.strategy,
+ True)
+ if dest_info is None:
+ return {'CANCELLED'}
+
+ # paste
+ ret = impl.paste_uv(self, bm, props.src_info, dest_info, uv_layers,
+ self.strategy, self.flip_copied_uv,
+ self.rotate_copied_uv, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
+
+ face_count = len(props.src_info[list(dest_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are pasted".format(face_count))
+
+ bmesh.update_edit_mesh(obj.data)
+ if self.copy_seams is True:
+ obj.data.show_edge_seams = True
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_CopyPasteUV_PasteUV(bpy.types.Menu):
+ """
+ Menu class: Paste UV coordinate
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_menu_paste_uv"
+ bl_label = "Paste UV (Menu)"
+ bl_description = "Menu of Paste UV coordinate"
+
+ @classmethod
+ def poll(cls, context):
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv
+ if not props.src_info:
+ return False
+ return impl.is_valid_context(context)
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+ # create sub menu
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+ uv_maps = bm.loops.layers.uv.keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname,
+ text="[New]")
+ ops.uv_map = "__new"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, text=m)
+ ops.uv_map = m
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_CopyPasteUV_SelSeqCopyUV(bpy.types.Operator):
+ """
+ Operation class: Copy UV coordinate by selection sequence
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_operator_selseq_copy_uv"
+ bl_label = "Copy UV (Selection Sequence)"
+ bl_description = "Copy UV data by selection sequence"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", options={'HIDDEN'})
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv_selseq
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ src_info = impl.get_select_history_src_face_info(self, bm, uv_layers)
+ if src_info is None:
+ return {'CANCELLED'}
+ props.src_info = src_info
+
+ face_count = len(props.src_info[list(props.src_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are selected".format(face_count))
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_CopyPasteUV_SelSeqCopyUV(bpy.types.Menu):
+ """
+ Menu class: Copy UV coordinate by selection sequence
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_menu_selseq_copy_uv"
+ bl_label = "Copy UV (Selection Sequence) (Menu)"
+ bl_description = "Menu of Copy UV coordinate by selection sequence"
+
+ @classmethod
+ def poll(cls, context):
+ return impl.is_valid_context(context)
+
+ def draw(self, context):
+ layout = self.layout
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+ uv_maps = bm.loops.layers.uv.keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text=m)
+ ops.uv_map = m
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_CopyPasteUV_SelSeqPasteUV(bpy.types.Operator):
+ """
+ Operation class: Paste UV coordinate by selection sequence
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_operator_selseq_paste_uv"
+ bl_label = "Paste UV (Selection Sequence)"
+ bl_description = "Paste UV coordinate by selection sequence"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", 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
+ )
+ copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_selseq
+ if not props.src_info:
+ return False
+ return impl.is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv_selseq
+ if not props.src_info:
+ self.report({'WARNING'}, "Need copy UV at first")
+ return {'CANCELLED'}
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info,
+ self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ dest_info = impl.get_select_history_dest_face_info(self, bm, uv_layers,
+ props.src_info,
+ self.strategy)
+ if dest_info is None:
+ return {'CANCELLED'}
+
+ # paste
+ ret = impl.paste_uv(self, bm, props.src_info, dest_info, uv_layers,
+ self.strategy, self.flip_copied_uv,
+ self.rotate_copied_uv, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
+
+ face_count = len(props.src_info[list(dest_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are pasted".format(face_count))
+
+ bmesh.update_edit_mesh(obj.data)
+ if self.copy_seams is True:
+ obj.data.show_edge_seams = True
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_CopyPasteUV_SelSeqPasteUV(bpy.types.Menu):
+ """
+ Menu class: Paste UV coordinate by selection sequence
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_menu_selseq_paste_uv"
+ bl_label = "Paste UV (Selection Sequence) (Menu)"
+ bl_description = "Menu of Paste UV coordinate by selection sequence"
+
+ @classmethod
+ def poll(cls, context):
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_selseq
+ if not props.src_uvs or not props.src_pin_uvs:
+ return False
+ return impl.is_valid_context(context)
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+ # create sub menu
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+ uv_maps = bm.loops.layers.uv.keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="[New]")
+ ops.uv_map = "__new"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text=m)
+ ops.uv_map = m
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
diff --git a/uv_magic_uv/legacy/op/copy_paste_uv_object.py b/uv_magic_uv/legacy/op/copy_paste_uv_object.py
new file mode 100644
index 00000000..e09b003b
--- /dev/null
+++ b/uv_magic_uv/legacy/op/copy_paste_uv_object.py
@@ -0,0 +1,298 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bmesh
+import bpy
+from bpy.props import (
+ StringProperty,
+ BoolProperty,
+)
+
+from ...impl import copy_paste_uv_impl as impl
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_CopyPasteUVObject_CopyUV',
+ 'MUV_MT_CopyPasteUVObject_CopyUV',
+ 'MUV_OT_CopyPasteUVObject_PasteUV',
+ 'MUV_MT_CopyPasteUVObject_PasteUV',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only object mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'OBJECT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "copy_paste_uv_object"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ src_info = None
+
+ scene.muv_props.copy_paste_uv_object = Props()
+
+ scene.muv_copy_paste_uv_object_copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.copy_paste_uv_object
+ del scene.muv_copy_paste_uv_object_copy_seams
+
+
+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
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_CopyPasteUVObject_CopyUV(bpy.types.Operator):
+ """
+ Operation class: Copy UV coordinate among objects
+ """
+
+ bl_idname = "object.muv_copy_paste_uv_object_operator_copy_uv"
+ bl_label = "Copy UV (Among Objects)"
+ bl_description = "Copy UV coordinate (Among Objects)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", options={'HIDDEN'})
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
+ @memorize_view_3d_mode
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv_object
+ bpy.ops.object.mode_set(mode='EDIT')
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ src_info = impl.get_src_face_info(self, bm, uv_layers)
+ if src_info is None:
+ return {'CANCELLED'}
+ props.src_info = src_info
+
+ self.report({'INFO'},
+ "{}'s UV coordinates are copied".format(obj.name))
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_CopyPasteUVObject_CopyUV(bpy.types.Menu):
+ """
+ Menu class: Copy UV coordinate among objects
+ """
+
+ bl_idname = "object.muv_copy_paste_uv_object_menu_copy_uv"
+ bl_label = "Copy UV (Among Objects) (Menu)"
+ bl_description = "Menu of Copy UV coordinate (Among Objects)"
+
+ @classmethod
+ def poll(cls, context):
+ return is_valid_context(context)
+
+ def draw(self, _):
+ layout = self.layout
+ # create sub menu
+ uv_maps = bpy.context.active_object.data.uv_textures.keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname,
+ text=m)
+ ops.uv_map = m
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_CopyPasteUVObject_PasteUV(bpy.types.Operator):
+ """
+ Operation class: Paste UV coordinate among objects
+ """
+
+ bl_idname = "object.muv_copy_paste_uv_object_operator_paste_uv"
+ bl_label = "Paste UV (Among Objects)"
+ bl_description = "Paste UV coordinate (Among Objects)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", options={'HIDDEN'})
+ copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_object
+ if not props.src_info:
+ return False
+ return is_valid_context(context)
+
+ @memorize_view_3d_mode
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv_object
+ if not props.src_info:
+ 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 = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info,
+ self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ dest_info = impl.get_dest_face_info(self, bm, uv_layers,
+ props.src_info, 'N_N')
+ if dest_info is None:
+ return {'CANCELLED'}
+
+ # paste
+ ret = impl.paste_uv(self, bm, props.src_info, dest_info, uv_layers,
+ 'N_N', 0, 0, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
+
+ bmesh.update_edit_mesh(obj.data)
+ if self.copy_seams is True:
+ obj.data.show_edge_seams = True
+
+ self.report(
+ {'INFO'}, "{}'s UV coordinates are pasted".format(obj.name))
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_CopyPasteUVObject_PasteUV(bpy.types.Menu):
+ """
+ Menu class: Paste UV coordinate among objects
+ """
+
+ bl_idname = "object.muv_copy_paste_uv_object_menu_paste_uv"
+ bl_label = "Paste UV (Among Objects) (Menu)"
+ bl_description = "Menu of Paste UV coordinate (Among Objects)"
+
+ @classmethod
+ def poll(cls, context):
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_object
+ if not props.src_info:
+ return False
+ return is_valid_context(context)
+
+ def draw(self, context):
+ sc = context.scene
+ 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())
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="[New]")
+ ops.uv_map = "__new"
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text=m)
+ ops.uv_map = m
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
diff --git a/uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py b/uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py
new file mode 100644
index 00000000..bb72d42a
--- /dev/null
+++ b/uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py
@@ -0,0 +1,97 @@
+# <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__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+from ...impl import copy_paste_uv_uvedit_impl as impl
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_CopyPasteUVUVEdit_CopyUV',
+ 'MUV_OT_CopyPasteUVUVEdit_PasteUV',
+]
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "copy_paste_uv_uvedit"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ src_uvs = None
+
+ scene.muv_props.copy_paste_uv_uvedit = Props()
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.copy_paste_uv_uvedit
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy.types.Operator):
+ """
+ Operation class: Copy UV coordinate on UV/Image Editor
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_uvedit_operator_copy_uv"
+ bl_label = "Copy UV (UV/Image Editor)"
+ bl_description = "Copy UV coordinate (only selected in UV/Image Editor)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def __init__(self):
+ self.__impl = impl.CopyUVImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.CopyUVImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator):
+ """
+ Operation class: Paste UV coordinate on UV/Image Editor
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_uvedit_operator_paste_uv"
+ bl_label = "Paste UV (UV/Image Editor)"
+ bl_description = "Paste UV coordinate (only selected in UV/Image Editor)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def __init__(self):
+ self.__impl = impl.PasteUVImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.PasteUVImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/op/flip_rotate_uv.py b/uv_magic_uv/legacy/op/flip_rotate_uv.py
new file mode 100644
index 00000000..d94e4808
--- /dev/null
+++ b/uv_magic_uv/legacy/op/flip_rotate_uv.py
@@ -0,0 +1,132 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+import bmesh
+from bpy.props import (
+ BoolProperty,
+ IntProperty,
+)
+
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+from ...impl import flip_rotate_impl as impl
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_FlipRotate',
+]
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "flip_rotate_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_flip_rotate_uv_enabled = BoolProperty(
+ name="Flip/Rotate UV Enabled",
+ description="Flip/Rotate UV is enabled",
+ default=False
+ )
+ scene.muv_flip_rotate_uv_seams = BoolProperty(
+ name="Seams",
+ description="Seams",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_flip_rotate_uv_enabled
+ del scene.muv_flip_rotate_uv_seams
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_FlipRotate(bpy.types.Operator):
+ """
+ Operation class: Flip and Rotate UV coordinate
+ """
+
+ bl_idname = "uv.muv_flip_rotate_uv_operator"
+ 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
+ )
+ seams = BoolProperty(
+ name="Seams",
+ description="Seams",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
+
+ def execute(self, context):
+ self.report({'INFO'}, "Flip/Rotate UV")
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # get UV layer
+ uv_layer = impl.get_uv_layer(self, bm)
+ if not uv_layer:
+ return {'CANCELLED'}
+
+ # get selected face
+ src_info = impl.get_src_face_info(self, bm, [uv_layer], True)
+ if not src_info:
+ return {'CANCELLED'}
+
+ face_count = len(src_info[list(src_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are selected".format(face_count))
+
+ # paste
+ ret = impl.paste_uv(self, bm, src_info, src_info, [uv_layer], 'N_N',
+ self.flip, self.rotate, self.seams)
+ if ret:
+ return {'CANCELLED'}
+
+ bmesh.update_edit_mesh(obj.data)
+ if self.seams is True:
+ obj.data.show_edge_seams = True
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/legacy/op/mirror_uv.py b/uv_magic_uv/legacy/op/mirror_uv.py
new file mode 100644
index 00000000..e869e5e8
--- /dev/null
+++ b/uv_magic_uv/legacy/op/mirror_uv.py
@@ -0,0 +1,110 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import (
+ EnumProperty,
+ FloatProperty,
+ BoolProperty,
+)
+
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+from ...impl import mirror_uv_impl as impl
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_MirrorUV',
+]
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "mirror_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_mirror_uv_enabled = BoolProperty(
+ name="Mirror UV Enabled",
+ description="Mirror UV is enabled",
+ default=False
+ )
+ scene.muv_mirror_uv_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'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_mirror_uv_enabled
+ del scene.muv_mirror_uv_axis
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_MirrorUV(bpy.types.Operator):
+ """
+ Operation class: Mirror UV
+ """
+
+ bl_idname = "uv.muv_mirror_uv_operator"
+ 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 __init__(self):
+ self.__impl = impl.MirrorUVImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.MirrorUVImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/op/move_uv.py b/uv_magic_uv/legacy/op/move_uv.py
new file mode 100644
index 00000000..2988c2ce
--- /dev/null
+++ b/uv_magic_uv/legacy/op/move_uv.py
@@ -0,0 +1,82 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import BoolProperty
+
+from ...impl import move_uv_impl as impl
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_MoveUV',
+]
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "move_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_move_uv_enabled = BoolProperty(
+ name="Move UV Enabled",
+ description="Move UV is enabled",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_move_uv_enabled
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_MoveUV(bpy.types.Operator):
+ """
+ Operator class: Move UV
+ """
+
+ bl_idname = "uv.muv_move_uv_operator"
+ bl_label = "Move UV"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def __init__(self):
+ self.__impl = impl.MoveUVImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.MoveUVImpl.poll(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return impl.MoveUVImpl.is_running(_)
+
+ def modal(self, context, event):
+ return self.__impl.modal(self, context, event)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/pack_uv.py b/uv_magic_uv/legacy/op/pack_uv.py
index a780af3e..f8d58843 100644
--- a/uv_magic_uv/op/pack_uv.py
+++ b/uv_magic_uv/legacy/op/pack_uv.py
@@ -20,8 +20,8 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
from math import fabs
@@ -35,10 +35,77 @@ from bpy.props import (
)
from mathutils import Vector
-from .. import common
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
-class MUV_PackUV(bpy.types.Operator):
+__all__ = [
+ 'Properties',
+ 'MUV_OT_PackUV',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "pack_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_pack_uv_enabled = BoolProperty(
+ name="Pack UV Enabled",
+ description="Pack UV is enabled",
+ default=False
+ )
+ scene.muv_pack_uv_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
+ )
+ scene.muv_pack_uv_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
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_pack_uv_enabled
+ del scene.muv_pack_uv_allowable_center_deviation
+ del scene.muv_pack_uv_allowable_size_deviation
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_PackUV(bpy.types.Operator):
"""
Operation class: Pack UV with same UV islands are integrated
Island matching algorithm
@@ -47,7 +114,7 @@ class MUV_PackUV(bpy.types.Operator):
- Same number of UV
"""
- bl_idname = "uv.muv_packuv"
+ bl_idname = "uv.muv_pack_uv_operator"
bl_label = "Pack UV"
bl_description = "Pack UV (Same UV Islands are integrated)"
bl_options = {'REGISTER', 'UNDO'}
@@ -79,6 +146,13 @@ class MUV_PackUV(bpy.types.Operator):
size=2
)
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
def execute(self, context):
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
diff --git a/uv_magic_uv/op/preserve_uv_aspect.py b/uv_magic_uv/legacy/op/preserve_uv_aspect.py
index bc2f1b81..cf9349bc 100644
--- a/uv_magic_uv/op/preserve_uv_aspect.py
+++ b/uv_magic_uv/legacy/op/preserve_uv_aspect.py
@@ -20,23 +20,99 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
import bmesh
-from bpy.props import StringProperty, EnumProperty
+from bpy.props import StringProperty, EnumProperty, BoolProperty
from mathutils import Vector
-from .. import common
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
-class MUV_PreserveUVAspect(bpy.types.Operator):
+__all__ = [
+ 'Properties',
+ 'MUV_OT_PreserveUVAspect',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "preserve_uv_aspect"
+
+ @classmethod
+ def init_props(cls, scene):
+ def get_loaded_texture_name(_, __):
+ items = [(key, key, "") for key in bpy.data.images.keys()]
+ items.append(("None", "None", ""))
+ return items
+
+ scene.muv_preserve_uv_aspect_enabled = BoolProperty(
+ name="Preserve UV Aspect Enabled",
+ description="Preserve UV Aspect is enabled",
+ default=False
+ )
+ scene.muv_preserve_uv_aspect_tex_image = EnumProperty(
+ name="Image",
+ description="Texture Image",
+ items=get_loaded_texture_name
+ )
+ scene.muv_preserve_uv_aspect_origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', 'Center', 'Center'),
+ ('LEFT_TOP', 'Left Top', 'Left Bottom'),
+ ('LEFT_CENTER', 'Left Center', 'Left Center'),
+ ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'),
+ ('CENTER_TOP', 'Center Top', 'Center Top'),
+ ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'),
+ ('RIGHT_TOP', 'Right Top', 'Right Top'),
+ ('RIGHT_CENTER', 'Right Center', 'Right Center'),
+ ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom')
+
+ ],
+ default="CENTER"
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_preserve_uv_aspect_enabled
+ del scene.muv_preserve_uv_aspect_tex_image
+ del scene.muv_preserve_uv_aspect_origin
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_PreserveUVAspect(bpy.types.Operator):
"""
Operation class: Preserve UV Aspect
"""
- bl_idname = "uv.muv_preserve_uv_aspect"
+ bl_idname = "uv.muv_preserve_uv_aspect_operator"
bl_label = "Preserve UV Aspect"
bl_description = "Choose Image"
bl_options = {'REGISTER', 'UNDO'}
@@ -62,8 +138,10 @@ class MUV_PreserveUVAspect(bpy.types.Operator):
@classmethod
def poll(cls, context):
- obj = context.active_object
- return obj and obj.type == 'MESH'
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
def execute(self, context):
# Note: the current system only works if the
diff --git a/uv_magic_uv/legacy/op/select_uv.py b/uv_magic_uv/legacy/op/select_uv.py
new file mode 100644
index 00000000..bdc182d5
--- /dev/null
+++ b/uv_magic_uv/legacy/op/select_uv.py
@@ -0,0 +1,168 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+import bmesh
+from bpy.props import BoolProperty
+
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_SelectUV_SelectFlipped',
+ 'MUV_OT_SelectUV_SelectOverlapped',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "select_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_select_uv_enabled = BoolProperty(
+ name="Select UV Enabled",
+ description="Select UV is enabled",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_select_uv_enabled
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_SelectUV_SelectOverlapped(bpy.types.Operator):
+ """
+ Operation class: Select faces which have overlapped UVs
+ """
+
+ bl_idname = "uv.muv_select_uv_operator_select_overlapped"
+ bl_label = "Overlapped"
+ bl_description = "Select faces which have overlapped UVs"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ uv_layer = bm.loops.layers.uv.verify()
+
+ if context.tool_settings.use_uv_select_sync:
+ sel_faces = [f for f in bm.faces]
+ else:
+ sel_faces = [f for f in bm.faces if f.select]
+
+ overlapped_info = common.get_overlapped_uv_info(bm, sel_faces,
+ uv_layer, 'FACE')
+
+ for info in overlapped_info:
+ if context.tool_settings.use_uv_select_sync:
+ info["subject_face"].select = True
+ else:
+ for l in info["subject_face"].loops:
+ l[uv_layer].select = True
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_SelectUV_SelectFlipped(bpy.types.Operator):
+ """
+ Operation class: Select faces which have flipped UVs
+ """
+
+ bl_idname = "uv.muv_select_uv_operator_select_flipped"
+ bl_label = "Flipped"
+ bl_description = "Select faces which have flipped UVs"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ uv_layer = bm.loops.layers.uv.verify()
+
+ if context.tool_settings.use_uv_select_sync:
+ sel_faces = [f for f in bm.faces]
+ else:
+ sel_faces = [f for f in bm.faces if f.select]
+
+ flipped_info = common.get_flipped_uv_info(sel_faces, uv_layer)
+
+ for info in flipped_info:
+ if context.tool_settings.use_uv_select_sync:
+ info["face"].select = True
+ else:
+ for l in info["face"].loops:
+ l[uv_layer].select = True
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/op/smooth_uv.py b/uv_magic_uv/legacy/op/smooth_uv.py
index aa9b22c0..63062554 100644
--- a/uv_magic_uv/op/smooth_uv.py
+++ b/uv_magic_uv/legacy/op/smooth_uv.py
@@ -20,19 +20,88 @@
__author__ = "imdjs, Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
import bmesh
from bpy.props import BoolProperty, FloatProperty
-from .. import common
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
-class MUV_AUVSmooth(bpy.types.Operator):
+__all__ = [
+ 'Properties',
+ 'MUV_OT_SmoothUV',
+]
- bl_idname = "uv.muv_auv_smooth"
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "smooth_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_smooth_uv_enabled = BoolProperty(
+ name="Smooth UV Enabled",
+ description="Smooth UV is enabled",
+ default=False
+ )
+ scene.muv_smooth_uv_transmission = BoolProperty(
+ name="Transmission",
+ description="Smooth linked UVs",
+ default=False
+ )
+ scene.muv_smooth_uv_mesh_infl = FloatProperty(
+ name="Mesh Influence",
+ description="Influence rate of mesh vertex",
+ min=0.0,
+ max=1.0,
+ default=0.0
+ )
+ scene.muv_smooth_uv_select = BoolProperty(
+ name="Select",
+ description="Select UVs which are smoothed",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_smooth_uv_enabled
+ del scene.muv_smooth_uv_transmission
+ del scene.muv_smooth_uv_mesh_infl
+ del scene.muv_smooth_uv_select
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_SmoothUV(bpy.types.Operator):
+
+ bl_idname = "uv.muv_smooth_uv_operator"
bl_label = "Smooth"
bl_description = "Smooth UV coordinates"
bl_options = {'REGISTER', 'UNDO'}
@@ -57,7 +126,10 @@ class MUV_AUVSmooth(bpy.types.Operator):
@classmethod
def poll(cls, context):
- return context.mode == 'EDIT_MESH'
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
def __smooth_wo_transmission(self, loop_seqs, uv_layer):
# calculate path length
diff --git a/uv_magic_uv/op/texture_lock.py b/uv_magic_uv/legacy/op/texture_lock.py
index d6c56f5a..65873106 100644
--- a/uv_magic_uv/op/texture_lock.py
+++ b/uv_magic_uv/legacy/op/texture_lock.py
@@ -20,8 +20,8 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import math
from math import atan2, cos, sqrt, sin, fabs
@@ -31,7 +31,17 @@ import bmesh
from mathutils import Vector
from bpy.props import BoolProperty
-from .. import common
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_TextureLock_Lock',
+ 'MUV_OT_TextureLock_Unlock',
+ 'MUV_OT_TextureLock_Intr',
+]
def get_vco(verts_orig, loop):
@@ -169,8 +179,13 @@ def calc_tri_vert(v0, v1, angle0, angle1):
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
+ r = s * (s - a) * (s - b) * (s - d)
+ if r < 0:
+ xd = 0
+ yd = 0
+ else:
+ xd = (b * b - a * a + d * d) / (2 * d)
+ yd = 2 * sqrt(r) / 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
@@ -179,18 +194,101 @@ def calc_tri_vert(v0, v1, angle0, angle1):
return Vector((x1, y1)), Vector((x2, y2))
-class MUV_TexLockStart(bpy.types.Operator):
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "texture_lock"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ verts_orig = None
+
+ scene.muv_props.texture_lock = Props()
+
+ def get_func(_):
+ return MUV_OT_TextureLock_Intr.is_running(bpy.context)
+
+ def set_func(_, __):
+ pass
+
+ def update_func(_, __):
+ bpy.ops.uv.muv_texture_lock_operator_intr('INVOKE_REGION_WIN')
+
+ scene.muv_texture_lock_enabled = BoolProperty(
+ name="Texture Lock Enabled",
+ description="Texture Lock is enabled",
+ default=False
+ )
+ scene.muv_texture_lock_lock = BoolProperty(
+ name="Texture Lock Locked",
+ description="Texture Lock is locked",
+ default=False,
+ get=get_func,
+ set=set_func,
+ update=update_func
+ )
+ scene.muv_texture_lock_connect = BoolProperty(
+ name="Connect UV",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.texture_lock
+ del scene.muv_texture_lock_enabled
+ del scene.muv_texture_lock_lock
+ del scene.muv_texture_lock_connect
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_TextureLock_Lock(bpy.types.Operator):
"""
- Operation class: Start Texture Lock
+ Operation class: Lock Texture
"""
- bl_idname = "uv.muv_texlock_start"
- bl_label = "Start"
- bl_description = "Start Texture Lock"
+ bl_idname = "uv.muv_texture_lock_operator_lock"
+ bl_label = "Lock Texture"
+ bl_description = "Lock Texture"
bl_options = {'REGISTER', 'UNDO'}
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
+ @classmethod
+ def is_ready(cls, context):
+ sc = context.scene
+ props = sc.muv_props.texture_lock
+ if props.verts_orig:
+ return True
+ return False
+
def execute(self, context):
- props = context.scene.muv_props.texlock
+ props = context.scene.muv_props.texture_lock
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if common.check_version(2, 73, 0) >= 0:
@@ -210,14 +308,15 @@ class MUV_TexLockStart(bpy.types.Operator):
return {'FINISHED'}
-class MUV_TexLockStop(bpy.types.Operator):
+@BlClassRegistry(legacy=True)
+class MUV_OT_TextureLock_Unlock(bpy.types.Operator):
"""
- Operation class: Stop Texture Lock
+ Operation class: Unlock Texture
"""
- bl_idname = "uv.muv_texlock_stop"
- bl_label = "Stop"
- bl_description = "Stop Texture Lock"
+ bl_idname = "uv.muv_texture_lock_operator_unlock"
+ bl_label = "Unlock Texture"
+ bl_description = "Unlock Texture"
bl_options = {'REGISTER', 'UNDO'}
connect = BoolProperty(
@@ -225,9 +324,24 @@ class MUV_TexLockStop(bpy.types.Operator):
default=True
)
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.texture_lock
+ if not props.verts_orig:
+ return False
+ if not MUV_OT_TextureLock_Lock.is_ready(context):
+ return False
+ if not is_valid_context(context):
+ return False
+ return True
+
def execute(self, context):
sc = context.scene
- props = sc.muv_props.texlock
+ props = sc.muv_props.texture_lock
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if common.check_version(2, 73, 0) >= 0:
@@ -275,27 +389,82 @@ class MUV_TexLockStop(bpy.types.Operator):
v_orig["moved"] = True
bmesh.update_edit_mesh(obj.data)
+ props.verts_orig = None
+
return {'FINISHED'}
-class MUV_TexLockUpdater(bpy.types.Operator):
+@BlClassRegistry(legacy=True)
+class MUV_OT_TextureLock_Intr(bpy.types.Operator):
"""
- Operation class: Texture locking updater
+ Operation class: Texture Lock (Interactive mode)
"""
- bl_idname = "uv.muv_texlock_updater"
- bl_label = "Texture Lock Updater"
- bl_description = "Texture Lock Updater"
+ bl_idname = "uv.muv_texture_lock_operator_intr"
+ bl_label = "Texture Lock (Interactive mode)"
+ bl_description = "Internal operation for Texture Lock (Interactive mode)"
+
+ __timer = None
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ return is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return 1 if cls.__timer else 0
+
+ @classmethod
+ def handle_add(cls, self_, context):
+ if cls.__timer is None:
+ cls.__timer = context.window_manager.event_timer_add(
+ 0.10, context.window)
+ context.window_manager.modal_handler_add(self_)
+
+ @classmethod
+ def handle_remove(cls, context):
+ if cls.__timer is not None:
+ context.window_manager.event_timer_remove(cls.__timer)
+ cls.__timer = None
def __init__(self):
- self.__timer = None
+ self.__intr_verts_orig = []
+ self.__intr_verts = []
+
+ def __sel_verts_changed(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ prev = set(self.__intr_verts)
+ now = set([v.index for v in bm.verts if v.select])
+
+ return prev != now
+
+ def __reinit_verts(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ self.__intr_verts_orig = [
+ {"vidx": v.index, "vco": v.co.copy(), "moved": False}
+ for v in bm.verts if v.select]
+ self.__intr_verts = [v.index for v in bm.verts if v.select]
def __update_uv(self, context):
"""
Update UV when vertex coordinates are changed
"""
- props = context.scene.muv_props.texlock
- obj = bpy.context.active_object
+ obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
@@ -308,7 +477,7 @@ class MUV_TexLockUpdater(bpy.types.Operator):
uv_layer = bm.loops.layers.uv.verify()
verts = [v.index for v in bm.verts if v.select]
- verts_orig = props.intr_verts_orig
+ verts_orig = self.__intr_verts_orig
for vidx, v_orig in zip(verts, verts_orig):
if vidx != v_orig["vidx"]:
@@ -337,98 +506,40 @@ class MUV_TexLockUpdater(bpy.types.Operator):
bmesh.update_edit_mesh(obj.data)
common.redraw_all_areas()
- props.intr_verts_orig = [
+ self.__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)
+ if not is_valid_context(context):
+ MUV_OT_TextureLock_Intr.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
+ if not MUV_OT_TextureLock_Intr.is_running(context):
+ return {'FINISHED'}
- 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'}
+ if event.type == 'TIMER':
+ if self.__sel_verts_changed(context):
+ self.__reinit_verts(context)
+ else:
+ self.__update_uv(context)
- obj = bpy.context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- if common.check_version(2, 73, 0) >= 0:
- bm.verts.ensure_lookup_table()
- bm.edges.ensure_lookup_table()
- bm.faces.ensure_lookup_table()
+ return {'PASS_THROUGH'}
- if not bm.loops.layers.uv:
- self.report({'WARNING'}, "Object must have more than one UV map")
+ def invoke(self, context, _):
+ if not is_valid_context(context):
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'}
+ if not MUV_OT_TextureLock_Intr.is_running(context):
+ MUV_OT_TextureLock_Intr.handle_add(self, context)
+ return {'RUNNING_MODAL'}
+ else:
+ MUV_OT_TextureLock_Intr.handle_remove(context)
- bpy.ops.uv.muv_texlock_updater()
+ if context.area:
+ context.area.tag_redraw()
return {'FINISHED'}
diff --git a/uv_magic_uv/legacy/op/texture_projection.py b/uv_magic_uv/legacy/op/texture_projection.py
new file mode 100644
index 00000000..58f69c9d
--- /dev/null
+++ b/uv_magic_uv/legacy/op/texture_projection.py
@@ -0,0 +1,402 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from collections import namedtuple
+
+import bpy
+import bgl
+import bmesh
+import mathutils
+from bpy_extras import view3d_utils
+from bpy.props import (
+ BoolProperty,
+ EnumProperty,
+ FloatProperty,
+)
+
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_TextureProjection',
+ 'MUV_OT_TextureProjection_Project',
+]
+
+
+Rect = namedtuple('Rect', 'x0 y0 x1 y1')
+Rect2 = namedtuple('Rect2', 'x y width height')
+
+
+def get_loaded_texture_name(_, __):
+ items = [(key, key, "") for key in bpy.data.images.keys()]
+ items.append(("None", "None", ""))
+ return items
+
+
+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.texture_projection_canvas_padding[0] * 2.0
+ canvas_h = region_h - prefs.texture_projection_canvas_padding[1] * 2.0
+
+ img = bpy.data.images[sc.muv_texture_projection_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_texture_projection_adjust_window:
+ ratio_x = canvas_w / tex_w
+ ratio_y = canvas_h / tex_h
+ if sc.muv_texture_projection_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_texture_projection_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
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "texture_projection"
+
+ @classmethod
+ def init_props(cls, scene):
+ def get_func(_):
+ return MUV_OT_TextureProjection.is_running(bpy.context)
+
+ def set_func(_, __):
+ pass
+
+ def update_func(_, __):
+ bpy.ops.uv.muv_texture_projection_operator('INVOKE_REGION_WIN')
+
+ scene.muv_texture_projection_enabled = BoolProperty(
+ name="Texture Projection Enabled",
+ description="Texture Projection is enabled",
+ default=False
+ )
+ scene.muv_texture_projection_enable = BoolProperty(
+ name="Texture Projection Enabled",
+ description="Texture Projection is enabled",
+ default=False,
+ get=get_func,
+ set=set_func,
+ update=update_func
+ )
+ scene.muv_texture_projection_tex_magnitude = FloatProperty(
+ name="Magnitude",
+ description="Texture Magnitude",
+ default=0.5,
+ min=0.0,
+ max=100.0
+ )
+ scene.muv_texture_projection_tex_image = EnumProperty(
+ name="Image",
+ description="Texture Image",
+ items=get_loaded_texture_name
+ )
+ scene.muv_texture_projection_tex_transparency = FloatProperty(
+ name="Transparency",
+ description="Texture Transparency",
+ default=0.2,
+ min=0.0,
+ max=1.0
+ )
+ scene.muv_texture_projection_adjust_window = BoolProperty(
+ name="Adjust Window",
+ description="Size of renderered texture is fitted to window",
+ default=True
+ )
+ scene.muv_texture_projection_apply_tex_aspect = BoolProperty(
+ name="Texture Aspect Ratio",
+ description="Apply Texture Aspect ratio to displayed texture",
+ default=True
+ )
+ scene.muv_texture_projection_assign_uvmap = BoolProperty(
+ name="Assign UVMap",
+ description="Assign UVMap when no UVmaps are available",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_texture_projection_enabled
+ del scene.muv_texture_projection_tex_magnitude
+ del scene.muv_texture_projection_tex_image
+ del scene.muv_texture_projection_tex_transparency
+ del scene.muv_texture_projection_adjust_window
+ del scene.muv_texture_projection_apply_tex_aspect
+ del scene.muv_texture_projection_assign_uvmap
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_TextureProjection(bpy.types.Operator):
+ """
+ Operation class: Texture Projection
+ Render texture
+ """
+
+ bl_idname = "uv.muv_texture_projection_operator"
+ bl_description = "Render selected texture"
+ bl_label = "Texture renderer"
+
+ __handle = None
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ return is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return 1 if cls.__handle else 0
+
+ @classmethod
+ def handle_add(cls, obj, context):
+ cls.__handle = bpy.types.SpaceView3D.draw_handler_add(
+ MUV_OT_TextureProjection.draw_texture,
+ (obj, context), 'WINDOW', 'POST_PIXEL')
+
+ @classmethod
+ def handle_remove(cls):
+ if cls.__handle is not None:
+ bpy.types.SpaceView3D.draw_handler_remove(cls.__handle, 'WINDOW')
+ cls.__handle = None
+
+ @classmethod
+ def draw_texture(cls, _, context):
+ sc = context.scene
+
+ if not cls.is_running(context):
+ return
+
+ # no textures are selected
+ if sc.muv_texture_projection_tex_image == "None":
+ return
+
+ # get texture to be renderred
+ img = bpy.data.images[sc.muv_texture_projection_tex_image]
+
+ # setup rendering region
+ rect = get_canvas(context, sc.muv_texture_projection_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_texture_projection_tex_transparency)
+ for (v1, v2), (u, v) in zip(positions, tex_coords):
+ bgl.glTexCoord2f(u, v)
+ bgl.glVertex2f(v1, v2)
+ bgl.glEnd()
+
+ def invoke(self, context, _):
+ if not MUV_OT_TextureProjection.is_running(context):
+ MUV_OT_TextureProjection.handle_add(self, context)
+ else:
+ MUV_OT_TextureProjection.handle_remove()
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_TextureProjection_Project(bpy.types.Operator):
+ """
+ Operation class: Project texture
+ """
+
+ bl_idname = "uv.muv_texture_projection_operator_project"
+ bl_label = "Project Texture"
+ bl_description = "Project Texture"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ if not MUV_OT_TextureProjection.is_running(context):
+ return False
+ return is_valid_context(context)
+
+ def execute(self, context):
+ sc = context.scene
+
+ if sc.muv_texture_projection_tex_image == "None":
+ self.report({'WARNING'}, "No textures are selected")
+ return {'CANCELLED'}
+
+ _, region, space = 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 common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # get UV and texture layer
+ if not bm.loops.layers.uv:
+ if sc.muv_texture_projection_assign_uvmap:
+ bm.loops.layers.uv.new()
+ else:
+ 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_texture_projection_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_texture_projection_tex_image]
+ for l in f.loops:
+ l[uv_layer].uv = v_canvas[i].to_2d()
+ i = i + 1
+
+ common.redraw_all_areas()
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/op/texture_wrap.py b/uv_magic_uv/legacy/op/texture_wrap.py
index 04669214..cb4cc78c 100644
--- a/uv_magic_uv/op/texture_wrap.py
+++ b/uv_magic_uv/legacy/op/texture_wrap.py
@@ -20,27 +20,104 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
import bmesh
-
-from .. import common
-
-
-class MUV_TexWrapRefer(bpy.types.Operator):
+from bpy.props import (
+ BoolProperty,
+)
+
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_TextureWrap_Refer',
+ 'MUV_OT_TextureWrap_Set',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "texture_wrap"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ ref_face_index = -1
+ ref_obj = None
+
+ scene.muv_props.texture_wrap = Props()
+
+ scene.muv_texture_wrap_enabled = BoolProperty(
+ name="Texture Wrap",
+ description="Texture Wrap is enabled",
+ default=False
+ )
+ scene.muv_texture_wrap_set_and_refer = BoolProperty(
+ name="Set and Refer",
+ description="Refer and set UV",
+ default=True
+ )
+ scene.muv_texture_wrap_selseq = BoolProperty(
+ name="Selection Sequence",
+ description="Set UV sequentially",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.texture_wrap
+ del scene.muv_texture_wrap_enabled
+ del scene.muv_texture_wrap_set_and_refer
+ del scene.muv_texture_wrap_selseq
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_TextureWrap_Refer(bpy.types.Operator):
"""
Operation class: Refer UV
"""
- bl_idname = "uv.muv_texwrap_refer"
+ bl_idname = "uv.muv_texture_wrap_operator_refer"
bl_label = "Refer"
bl_description = "Refer UV"
bl_options = {'REGISTER', 'UNDO'}
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
def execute(self, context):
- props = context.scene.muv_props.texwrap
+ props = context.scene.muv_props.texture_wrap
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if common.check_version(2, 73, 0) >= 0:
@@ -61,19 +138,31 @@ class MUV_TexWrapRefer(bpy.types.Operator):
return {'FINISHED'}
-class MUV_TexWrapSet(bpy.types.Operator):
+@BlClassRegistry(legacy=True)
+class MUV_OT_TextureWrap_Set(bpy.types.Operator):
"""
Operation class: Set UV
"""
- bl_idname = "uv.muv_texwrap_set"
+ bl_idname = "uv.muv_texture_wrap_operator_set"
bl_label = "Set"
bl_description = "Set UV"
bl_options = {'REGISTER', 'UNDO'}
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.texture_wrap
+ if not props.ref_obj:
+ return False
+ return is_valid_context(context)
+
def execute(self, context):
sc = context.scene
- props = sc.muv_props.texwrap
+ props = sc.muv_props.texture_wrap
obj = context.active_object
bm = bmesh.from_edit_mesh(obj.data)
if common.check_version(2, 73, 0) >= 0:
@@ -84,7 +173,7 @@ class MUV_TexWrapSet(bpy.types.Operator):
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
- if sc.muv_texwrap_selseq:
+ if sc.muv_texture_wrap_selseq:
sel_faces = []
for hist in bm.select_history:
if isinstance(hist, bmesh.types.BMFace) and hist.select:
@@ -206,7 +295,7 @@ class MUV_TexWrapSet(bpy.types.Operator):
ref_face_index = tgt_face_index
- if sc.muv_texwrap_set_and_refer:
+ if sc.muv_texture_wrap_set_and_refer:
props.ref_face_index = tgt_face_index
return {'FINISHED'}
diff --git a/uv_magic_uv/legacy/op/transfer_uv.py b/uv_magic_uv/legacy/op/transfer_uv.py
new file mode 100644
index 00000000..cd0e4dd9
--- /dev/null
+++ b/uv_magic_uv/legacy/op/transfer_uv.py
@@ -0,0 +1,172 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+import bmesh
+from bpy.props import BoolProperty
+
+from ... import common
+from ...impl import transfer_uv_impl as impl
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_TransferUV_CopyUV',
+ 'MUV_OT_TransferUV_PasteUV',
+]
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "transfer_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ topology_copied = None
+
+ scene.muv_props.transfer_uv = Props()
+
+ scene.muv_transfer_uv_enabled = BoolProperty(
+ name="Transfer UV Enabled",
+ description="Transfer UV is enabled",
+ default=False
+ )
+ scene.muv_transfer_uv_invert_normals = BoolProperty(
+ name="Invert Normals",
+ description="Invert Normals",
+ default=False
+ )
+ scene.muv_transfer_uv_copy_seams = BoolProperty(
+ name="Copy Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_transfer_uv_enabled
+ del scene.muv_transfer_uv_invert_normals
+ del scene.muv_transfer_uv_copy_seams
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_TransferUV_CopyUV(bpy.types.Operator):
+ """
+ Operation class: Transfer UV copy
+ Topological based copy
+ """
+
+ bl_idname = "uv.muv_transfer_uv_operator_copy_uv"
+ bl_label = "Transfer UV Copy UV"
+ bl_description = "Transfer UV Copy UV (Topological based copy)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.transfer_uv
+ active_obj = context.scene.objects.active
+ bm = bmesh.from_edit_mesh(active_obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ uv_layer = impl.get_uv_layer(self, bm)
+ if uv_layer is None:
+ return {'CANCELLED'}
+
+ faces = impl.get_selected_src_faces(self, bm, uv_layer)
+ if faces is None:
+ return {'CANCELLED'}
+ props.topology_copied = faces
+
+ bmesh.update_edit_mesh(active_obj.data)
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_TransferUV_PasteUV(bpy.types.Operator):
+ """
+ Operation class: Transfer UV paste
+ Topological based paste
+ """
+
+ bl_idname = "uv.muv_transfer_uv_operator_paste_uv"
+ bl_label = "Transfer UV Paste UV"
+ bl_description = "Transfer UV Paste UV (Topological based paste)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ invert_normals = BoolProperty(
+ name="Invert Normals",
+ description="Invert Normals",
+ default=False
+ )
+ copy_seams = BoolProperty(
+ name="Copy Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.transfer_uv
+ if not props.topology_copied:
+ return False
+ return impl.is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.transfer_uv
+ active_obj = context.scene.objects.active
+ bm = bmesh.from_edit_mesh(active_obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # get UV layer
+ uv_layer = impl.get_uv_layer(self, bm)
+ if uv_layer is None:
+ return {'CANCELLED'}
+
+ ret = impl.paste_uv(self, bm, uv_layer, props.topology_copied,
+ self.invert_normals, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
+
+ bmesh.update_edit_mesh(active_obj.data)
+ if self.copy_seams:
+ active_obj.data.show_edge_seams = True
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/op/unwrap_constraint.py b/uv_magic_uv/legacy/op/unwrap_constraint.py
index e98879b7..f06efce1 100644
--- a/uv_magic_uv/op/unwrap_constraint.py
+++ b/uv_magic_uv/legacy/op/unwrap_constraint.py
@@ -18,8 +18,8 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
import bmesh
@@ -29,15 +29,74 @@ from bpy.props import (
FloatProperty,
)
-from .. import common
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
-class MUV_UnwrapConstraint(bpy.types.Operator):
+__all__ = [
+ 'Properties',
+ 'MUV_OT_UnwrapConstraint',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "unwrap_constraint"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_unwrap_constraint_enabled = BoolProperty(
+ name="Unwrap Constraint Enabled",
+ description="Unwrap Constraint is enabled",
+ default=False
+ )
+ scene.muv_unwrap_constraint_u_const = BoolProperty(
+ name="U-Constraint",
+ description="Keep UV U-axis coordinate",
+ default=False
+ )
+ scene.muv_unwrap_constraint_v_const = BoolProperty(
+ name="V-Constraint",
+ description="Keep UV V-axis coordinate",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_unwrap_constraint_enabled
+ del scene.muv_unwrap_constraint_u_const
+ del scene.muv_unwrap_constraint_v_const
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_UnwrapConstraint(bpy.types.Operator):
"""
Operation class: Unwrap with constrain UV coordinate
"""
- bl_idname = "uv.muv_unwrap_constraint"
+ bl_idname = "uv.muv_unwrap_constraint_operator"
bl_label = "Unwrap Constraint"
bl_description = "Unwrap while keeping uv coordinate"
bl_options = {'REGISTER', 'UNDO'}
@@ -83,6 +142,13 @@ class MUV_UnwrapConstraint(bpy.types.Operator):
default=False
)
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
def execute(self, _):
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
diff --git a/uv_magic_uv/op/uv_bounding_box.py b/uv_magic_uv/legacy/op/uv_bounding_box.py
index 9ebc76c4..47c27e41 100644
--- a/uv_magic_uv/op/uv_bounding_box.py
+++ b/uv_magic_uv/legacy/op/uv_bounding_box.py
@@ -20,8 +20,8 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
from enum import IntEnum
import math
@@ -30,14 +30,106 @@ import bpy
import bgl
import mathutils
import bmesh
+from bpy.props import BoolProperty, EnumProperty
-from .. import common
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_UVBoundingBox',
+]
MAX_VALUE = 100000.0
-class MUV_UVBBCmd():
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "uv_bounding_box"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ uv_info_ini = []
+ ctrl_points_ini = []
+ ctrl_points = []
+
+ scene.muv_props.uv_bounding_box = Props()
+
+ def get_func(_):
+ return MUV_OT_UVBoundingBox.is_running(bpy.context)
+
+ def set_func(_, __):
+ pass
+
+ def update_func(_, __):
+ bpy.ops.uv.muv_uv_bounding_box_operator('INVOKE_REGION_WIN')
+
+ scene.muv_uv_bounding_box_enabled = BoolProperty(
+ name="UV Bounding Box Enabled",
+ description="UV Bounding Box is enabled",
+ default=False
+ )
+ scene.muv_uv_bounding_box_show = BoolProperty(
+ name="UV Bounding Box Showed",
+ description="UV Bounding Box is showed",
+ default=False,
+ get=get_func,
+ set=set_func,
+ update=update_func
+ )
+ scene.muv_uv_bounding_box_uniform_scaling = BoolProperty(
+ name="Uniform Scaling",
+ description="Enable Uniform Scaling",
+ default=False
+ )
+ scene.muv_uv_bounding_box_boundary = EnumProperty(
+ name="Boundary",
+ description="Boundary",
+ default='UV_SEL',
+ items=[
+ ('UV', "UV", "Boundary is decided by UV"),
+ ('UV_SEL', "UV (Selected)",
+ "Boundary is decided by Selected UV")
+ ]
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.uv_bounding_box
+ del scene.muv_uv_bounding_box_enabled
+ del scene.muv_uv_bounding_box_show
+ del scene.muv_uv_bounding_box_uniform_scaling
+ del scene.muv_uv_bounding_box_boundary
+
+
+class CommandBase():
"""
Custom class: Base class of command
"""
@@ -52,7 +144,7 @@ class MUV_UVBBCmd():
return mat
-class MUV_UVBBTranslationCmd(MUV_UVBBCmd):
+class TranslationCommand(CommandBase):
"""
Custom class: Translation operation
"""
@@ -76,7 +168,7 @@ class MUV_UVBBTranslationCmd(MUV_UVBBCmd):
self.__y = y
-class MUV_UVBBRotationCmd(MUV_UVBBCmd):
+class RotationCommand(CommandBase):
"""
Custom class: Rotation operation
"""
@@ -107,7 +199,7 @@ class MUV_UVBBRotationCmd(MUV_UVBBCmd):
self.__y = y
-class MUV_UVBBScalingCmd(MUV_UVBBCmd):
+class ScalingCommand(CommandBase):
"""
Custom class: Scaling operation
"""
@@ -158,7 +250,7 @@ class MUV_UVBBScalingCmd(MUV_UVBBCmd):
self.__y = y
-class MUV_UVBBUniformScalingCmd(MUV_UVBBCmd):
+class UniformScalingCommand(CommandBase):
"""
Custom class: Uniform Scaling operation
"""
@@ -222,7 +314,7 @@ class MUV_UVBBUniformScalingCmd(MUV_UVBBCmd):
self.__y = y
-class MUV_UVBBCmdExecuter():
+class CommandExecuter():
"""
Custom class: manage command history and execute command
"""
@@ -288,67 +380,7 @@ class MUV_UVBBCmdExecuter():
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):
+class State(IntEnum):
"""
Enum: State definition used by MUV_UVBBStateMgr
"""
@@ -369,7 +401,7 @@ class MUV_UVBBState(IntEnum):
UNIFORM_SCALING_4 = 14
-class MUV_UVBBStateBase():
+class StateBase():
"""
Custom class: Base class of state
"""
@@ -381,7 +413,7 @@ class MUV_UVBBStateBase():
raise NotImplementedError
-class MUV_UVBBStateNone(MUV_UVBBStateBase):
+class StateNone(StateBase):
"""
Custom class:
No state
@@ -397,8 +429,8 @@ class MUV_UVBBStateNone(MUV_UVBBStateBase):
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
+ cp_react_size = prefs.uv_bounding_box_cp_react_size
+ is_uscaling = context.scene.muv_uv_bounding_box_uniform_scaling
if (event.type == 'LEFTMOUSE') and (event.value == 'PRESS'):
x, y = context.region.view2d.view_to_region(
mouse_view.x, mouse_view.y)
@@ -413,16 +445,16 @@ class MUV_UVBBStateNone(MUV_UVBBStateBase):
arr = [1, 3, 6, 8]
if i in arr:
return (
- MUV_UVBBState.UNIFORM_SCALING_1 +
+ State.UNIFORM_SCALING_1 +
arr.index(i)
)
else:
- return MUV_UVBBState.TRANSLATING + i
+ return State.TRANSLATING + i
- return MUV_UVBBState.NONE
+ return State.NONE
-class MUV_UVBBStateTranslating(MUV_UVBBStateBase):
+class StateTranslating(StateBase):
"""
Custom class: Translating state
"""
@@ -431,19 +463,19 @@ class MUV_UVBBStateTranslating(MUV_UVBBStateBase):
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))
+ self.__cmd_exec.append(TranslationCommand(ix, iy))
def update(self, context, event, ctrl_points, mouse_view):
if event.type == 'LEFTMOUSE':
if event.value == 'RELEASE':
- return MUV_UVBBState.NONE
+ return State.NONE
if event.type == 'MOUSEMOVE':
x, y = mouse_view.x, mouse_view.y
self.__cmd_exec.top().set(x, y)
- return MUV_UVBBState.TRANSLATING
+ return State.TRANSLATING
-class MUV_UVBBStateScaling(MUV_UVBBStateBase):
+class StateScaling(StateBase):
"""
Custom class: Scaling state
"""
@@ -460,19 +492,19 @@ class MUV_UVBBStateScaling(MUV_UVBBStateBase):
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()))
+ ScalingCommand(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
+ return State.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):
+class StateUniformScaling(StateBase):
"""
Custom class: Uniform Scaling state
"""
@@ -483,17 +515,17 @@ class MUV_UVBBStateUniformScaling(MUV_UVBBStateBase):
self.__cmd_exec = cmd_exec
icp_idx = [1, 3, 6, 8]
ocp_idx = [8, 6, 3, 1]
- idx = state - MUV_UVBBState.UNIFORM_SCALING_1
+ idx = state - State.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(
+ self.__cmd_exec.append(UniformScalingCommand(
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
+ return State.NONE
if event.type == 'MOUSEMOVE':
x, y = mouse_view.x, mouse_view.y
self.__cmd_exec.top().set(x, y)
@@ -501,7 +533,7 @@ class MUV_UVBBStateUniformScaling(MUV_UVBBStateBase):
return self.__state
-class MUV_UVBBStateRotating(MUV_UVBBStateBase):
+class StateRotating(StateBase):
"""
Custom class: Rotating state
"""
@@ -511,27 +543,27 @@ class MUV_UVBBStateRotating(MUV_UVBBStateBase):
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))
+ self.__cmd_exec.append(RotationCommand(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
+ return State.NONE
if event.type == 'MOUSEMOVE':
x, y = mouse_view.x, mouse_view.y
self.__cmd_exec.top().set(x, y)
- return MUV_UVBBState.ROTATING
+ return State.ROTATING
-class MUV_UVBBStateMgr():
+class StateManager():
"""
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)
+ self.__state = State.NONE # current state
+ self.__state_obj = StateNone(self.__cmd_exec)
def __update_state(self, next_state, ctrl_points):
"""
@@ -541,18 +573,18 @@ class MUV_UVBBStateMgr():
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(
+ if next_state == State.TRANSLATING:
+ obj = StateTranslating(self.__cmd_exec, ctrl_points)
+ elif State.SCALING_1 <= next_state <= State.SCALING_8:
+ obj = StateScaling(
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(
+ elif next_state == State.ROTATING:
+ obj = StateRotating(self.__cmd_exec, ctrl_points)
+ elif next_state == State.NONE:
+ obj = StateNone(self.__cmd_exec)
+ elif (State.UNIFORM_SCALING_1 <= next_state <=
+ State.UNIFORM_SCALING_4):
+ obj = StateUniformScaling(
self.__cmd_exec, next_state, ctrl_points)
if obj is not None:
@@ -569,34 +601,98 @@ class MUV_UVBBStateMgr():
context, event, ctrl_points, mouse_view)
self.__update_state(next_state, ctrl_points)
+ return self.__state
+
-class MUV_UVBBUpdater(bpy.types.Operator):
+@BlClassRegistry(legacy=True)
+class MUV_OT_UVBoundingBox(bpy.types.Operator):
"""
- Operation class: Update state and handle event by modal function
+ Operation class: UV Bounding Box
"""
- bl_idname = "uv.muv_uvbb_updater"
- bl_label = "UV Bounding Box Updater"
- bl_description = "Update UV Bounding Box"
+ bl_idname = "uv.muv_uv_bounding_box_operator"
+ bl_label = "UV Bounding Box"
+ bl_description = "Internal operation for 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
+ self.__cmd_exec = CommandExecuter() # Command executor
+ self.__state_mgr = StateManager(self.__cmd_exec) # State Manager
- def __handle_add(self, context):
- if self.__timer is None:
- self.__timer = context.window_manager.event_timer_add(
+ __handle = None
+ __timer = None
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ return is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return 1 if cls.__handle else 0
+
+ @classmethod
+ def handle_add(cls, obj, context):
+ if cls.__handle is None:
+ sie = bpy.types.SpaceImageEditor
+ cls.__handle = sie.draw_handler_add(
+ cls.draw_bb, (obj, context), "WINDOW", "POST_PIXEL")
+ if cls.__timer is None:
+ cls.__timer = context.window_manager.event_timer_add(
0.1, context.window)
- context.window_manager.modal_handler_add(self)
- MUV_UVBBRenderer.handle_add(self, context)
+ context.window_manager.modal_handler_add(obj)
- 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
+ @classmethod
+ def handle_remove(cls, context):
+ if cls.__handle is not None:
+ sie = bpy.types.SpaceImageEditor
+ sie.draw_handler_remove(cls.__handle, "WINDOW")
+ cls.__handle = None
+ if cls.__timer is not None:
+ context.window_manager.event_timer_remove(cls.__timer)
+ cls.__timer = None
+
+ @classmethod
+ def __draw_ctrl_point(cls, context, pos):
+ """
+ Draw control point
+ """
+ prefs = context.user_preferences.addons["uv_magic_uv"].preferences
+ cp_size = prefs.uv_bounding_box_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()
+
+ @classmethod
+ def draw_bb(cls, _, context):
+ """
+ Draw bounding box
+ """
+ props = context.scene.muv_props.uv_bounding_box
+
+ if not MUV_OT_UVBoundingBox.is_running(context):
+ return
+
+ if not is_valid_context(context):
+ return
+
+ for cp in props.ctrl_points:
+ cls.__draw_ctrl_point(
+ context, mathutils.Vector(
+ context.region.view2d.view_to_region(cp.x, cp.y)))
def __get_uv_info(self, context):
"""
@@ -615,10 +711,10 @@ class MUV_UVBBUpdater(bpy.types.Operator):
if not f.select:
continue
for i, l in enumerate(f.loops):
- if sc.muv_uvbb_boundary == 'UV_SEL':
+ if sc.muv_uv_bounding_box_boundary == 'UV_SEL':
if l[uv_layer].select:
uv_info.append((f.index, i, l[uv_layer].uv.copy()))
- elif sc.muv_uvbb_boundary == 'UV':
+ elif sc.muv_uv_bounding_box_boundary == 'UV':
uv_info.append((f.index, i, l[uv_layer].uv.copy()))
if not uv_info:
return None
@@ -688,16 +784,23 @@ class MUV_UVBBUpdater(bpy.types.Operator):
return [trans_mat * cp for cp in ctrl_points_ini]
def modal(self, context, event):
- props = context.scene.muv_props.uvbb
+ props = context.scene.muv_props.uv_bounding_box
common.redraw_all_areas()
- if props.running is False:
- self.__handle_remove(context)
+
+ if not MUV_OT_UVBoundingBox.is_running(context):
return {'FINISHED'}
- area, _, _ = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
+ if not is_valid_context(context):
+ MUV_OT_UVBoundingBox.handle_remove(context)
+ return {'FINISHED'}
- if event.mouse_region_x < 0 or event.mouse_region_x > area.width or \
- event.mouse_region_y < 0 or event.mouse_region_y > area.height:
+ region_types = [
+ 'HEADER',
+ 'UI',
+ 'TOOLS',
+ ]
+ if not common.mouse_on_area(event, 'IMAGE_EDITOR') or \
+ common.mouse_on_regions(event, 'IMAGE_EDITOR', region_types):
return {'PASS_THROUGH'}
if event.type == 'TIMER':
@@ -706,27 +809,30 @@ class MUV_UVBBUpdater(bpy.types.Operator):
props.ctrl_points = self.__update_ctrl_point(
props.ctrl_points_ini, trans_mat)
- self.__state_mgr.update(context, props.ctrl_points, event)
+ state = self.__state_mgr.update(context, props.ctrl_points, event)
+ if state == State.NONE:
+ return {'PASS_THROUGH'}
return {'RUNNING_MODAL'}
- def execute(self, context):
- props = context.scene.muv_props.uvbb
+ def invoke(self, context, _):
+ props = context.scene.muv_props.uv_bounding_box
- if props.running is True:
- props.running = False
+ if MUV_OT_UVBoundingBox.is_running(context):
+ MUV_OT_UVBoundingBox.handle_remove(context)
return {'FINISHED'}
props.uv_info_ini = self.__get_uv_info(context)
if props.uv_info_ini is None:
return {'CANCELLED'}
+
+ MUV_OT_UVBoundingBox.handle_add(self, context)
+
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'}
diff --git a/uv_magic_uv/legacy/op/uv_inspection.py b/uv_magic_uv/legacy/op/uv_inspection.py
new file mode 100644
index 00000000..57d42468
--- /dev/null
+++ b/uv_magic_uv/legacy/op/uv_inspection.py
@@ -0,0 +1,280 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+import bmesh
+import bgl
+from bpy.props import BoolProperty, EnumProperty
+
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_UVInspection_Render',
+ 'MUV_OT_UVInspection_Update',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "uv_inspection"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ overlapped_info = []
+ flipped_info = []
+
+ scene.muv_props.uv_inspection = Props()
+
+ def get_func(_):
+ return MUV_OT_UVInspection_Render.is_running(bpy.context)
+
+ def set_func(_, __):
+ pass
+
+ def update_func(_, __):
+ bpy.ops.uv.muv_uv_inspection_operator_render('INVOKE_REGION_WIN')
+
+ scene.muv_uv_inspection_enabled = BoolProperty(
+ name="UV Inspection Enabled",
+ description="UV Inspection is enabled",
+ default=False
+ )
+ scene.muv_uv_inspection_show = BoolProperty(
+ name="UV Inspection Showed",
+ description="UV Inspection is showed",
+ default=False,
+ get=get_func,
+ set=set_func,
+ update=update_func
+ )
+ scene.muv_uv_inspection_show_overlapped = BoolProperty(
+ name="Overlapped",
+ description="Show overlapped UVs",
+ default=False
+ )
+ scene.muv_uv_inspection_show_flipped = BoolProperty(
+ name="Flipped",
+ description="Show flipped UVs",
+ default=False
+ )
+ scene.muv_uv_inspection_show_mode = EnumProperty(
+ name="Mode",
+ description="Show mode",
+ items=[
+ ('PART', "Part", "Show only overlapped/flipped part"),
+ ('FACE', "Face", "Show overlapped/flipped face")
+ ],
+ default='PART'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.uv_inspection
+ del scene.muv_uv_inspection_enabled
+ del scene.muv_uv_inspection_show
+ del scene.muv_uv_inspection_show_overlapped
+ del scene.muv_uv_inspection_show_flipped
+ del scene.muv_uv_inspection_show_mode
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_UVInspection_Render(bpy.types.Operator):
+ """
+ Operation class: Render UV Inspection
+ No operation (only rendering)
+ """
+
+ bl_idname = "uv.muv_uv_inspection_operator_render"
+ bl_description = "Render overlapped/flipped UVs"
+ bl_label = "Overlapped/Flipped UV renderer"
+
+ __handle = None
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ return is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return 1 if cls.__handle else 0
+
+ @classmethod
+ def handle_add(cls, obj, context):
+ sie = bpy.types.SpaceImageEditor
+ cls.__handle = sie.draw_handler_add(
+ MUV_OT_UVInspection_Render.draw, (obj, context),
+ 'WINDOW', 'POST_PIXEL')
+
+ @classmethod
+ def handle_remove(cls):
+ if cls.__handle is not None:
+ bpy.types.SpaceImageEditor.draw_handler_remove(
+ cls.__handle, 'WINDOW')
+ cls.__handle = None
+
+ @staticmethod
+ def draw(_, context):
+ sc = context.scene
+ props = sc.muv_props.uv_inspection
+ prefs = context.user_preferences.addons["uv_magic_uv"].preferences
+
+ if not MUV_OT_UVInspection_Render.is_running(context):
+ return
+
+ # OpenGL configuration
+ bgl.glEnable(bgl.GL_BLEND)
+
+ # render overlapped UV
+ if sc.muv_uv_inspection_show_overlapped:
+ color = prefs.uv_inspection_overlapped_color
+ for info in props.overlapped_info:
+ if sc.muv_uv_inspection_show_mode == 'PART':
+ for poly in info["polygons"]:
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ bgl.glColor4f(color[0], color[1], color[2], color[3])
+ for uv in poly:
+ x, y = context.region.view2d.view_to_region(
+ uv.x, uv.y)
+ bgl.glVertex2f(x, y)
+ bgl.glEnd()
+ elif sc.muv_uv_inspection_show_mode == 'FACE':
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ bgl.glColor4f(color[0], color[1], color[2], color[3])
+ for uv in info["subject_uvs"]:
+ x, y = context.region.view2d.view_to_region(uv.x, uv.y)
+ bgl.glVertex2f(x, y)
+ bgl.glEnd()
+
+ # render flipped UV
+ if sc.muv_uv_inspection_show_flipped:
+ color = prefs.uv_inspection_flipped_color
+ for info in props.flipped_info:
+ if sc.muv_uv_inspection_show_mode == 'PART':
+ for poly in info["polygons"]:
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ bgl.glColor4f(color[0], color[1], color[2], color[3])
+ for uv in poly:
+ x, y = context.region.view2d.view_to_region(
+ uv.x, uv.y)
+ bgl.glVertex2f(x, y)
+ bgl.glEnd()
+ elif sc.muv_uv_inspection_show_mode == 'FACE':
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ bgl.glColor4f(color[0], color[1], color[2], color[3])
+ for uv in info["uvs"]:
+ x, y = context.region.view2d.view_to_region(uv.x, uv.y)
+ bgl.glVertex2f(x, y)
+ bgl.glEnd()
+
+ def invoke(self, context, _):
+ if not MUV_OT_UVInspection_Render.is_running(context):
+ update_uvinsp_info(context)
+ MUV_OT_UVInspection_Render.handle_add(self, context)
+ else:
+ MUV_OT_UVInspection_Render.handle_remove()
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'FINISHED'}
+
+
+def update_uvinsp_info(context):
+ sc = context.scene
+ props = sc.muv_props.uv_inspection
+
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ uv_layer = bm.loops.layers.uv.verify()
+
+ if context.tool_settings.use_uv_select_sync:
+ sel_faces = [f for f in bm.faces]
+ else:
+ sel_faces = [f for f in bm.faces if f.select]
+ props.overlapped_info = common.get_overlapped_uv_info(
+ bm, sel_faces, uv_layer, sc.muv_uv_inspection_show_mode)
+ props.flipped_info = common.get_flipped_uv_info(sel_faces, uv_layer)
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_UVInspection_Update(bpy.types.Operator):
+ """
+ Operation class: Update
+ """
+
+ bl_idname = "uv.muv_uv_inspection_operator_update"
+ bl_label = "Update UV Inspection"
+ bl_description = "Update UV Inspection"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ if not MUV_OT_UVInspection_Render.is_running(context):
+ return False
+ return is_valid_context(context)
+
+ def execute(self, context):
+ update_uvinsp_info(context)
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/op/uv_sculpt.py b/uv_magic_uv/legacy/op/uv_sculpt.py
index 2bf76abd..3754a759 100644
--- a/uv_magic_uv/op/uv_sculpt.py
+++ b/uv_magic_uv/legacy/op/uv_sculpt.py
@@ -20,8 +20,8 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
from math import pi, cos, tan, sin
@@ -32,39 +32,178 @@ from mathutils import Vector
from bpy_extras import view3d_utils
from mathutils.bvhtree import BVHTree
from mathutils.geometry import barycentric_transform
-
-from .. import common
-
-
-class MUV_UVSculptRenderer(bpy.types.Operator):
+from bpy.props import (
+ BoolProperty,
+ IntProperty,
+ EnumProperty,
+ FloatProperty,
+)
+
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_UVSculpt',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "uv_sculpt"
+
+ @classmethod
+ def init_props(cls, scene):
+ def get_func(_):
+ return MUV_OT_UVSculpt.is_running(bpy.context)
+
+ def set_func(_, __):
+ pass
+
+ def update_func(_, __):
+ bpy.ops.uv.muv_uv_sculpt_operator('INVOKE_REGION_WIN')
+
+ scene.muv_uv_sculpt_enabled = BoolProperty(
+ name="UV Sculpt",
+ description="UV Sculpt is enabled",
+ default=False
+ )
+ scene.muv_uv_sculpt_enable = BoolProperty(
+ name="UV Sculpt Showed",
+ description="UV Sculpt is enabled",
+ default=False,
+ get=get_func,
+ set=set_func,
+ update=update_func
+ )
+ scene.muv_uv_sculpt_radius = IntProperty(
+ name="Radius",
+ description="Radius of the brush",
+ min=1,
+ max=500,
+ default=30
+ )
+ scene.muv_uv_sculpt_strength = FloatProperty(
+ name="Strength",
+ description="How powerful the effect of the brush when applied",
+ min=0.0,
+ max=1.0,
+ default=0.03,
+ )
+ scene.muv_uv_sculpt_tools = EnumProperty(
+ name="Tools",
+ description="Select Tools for the UV sculpt brushes",
+ items=[
+ ('GRAB', "Grab", "Grab UVs"),
+ ('RELAX', "Relax", "Relax UVs"),
+ ('PINCH', "Pinch", "Pinch UVs")
+ ],
+ default='GRAB'
+ )
+ scene.muv_uv_sculpt_show_brush = BoolProperty(
+ name="Show Brush",
+ description="Show Brush",
+ default=True
+ )
+ scene.muv_uv_sculpt_pinch_invert = BoolProperty(
+ name="Invert",
+ description="Pinch UV to invert direction",
+ default=False
+ )
+ scene.muv_uv_sculpt_relax_method = EnumProperty(
+ name="Method",
+ description="Algorithm used for relaxation",
+ items=[
+ ('HC', "HC", "Use HC method for relaxation"),
+ ('LAPLACIAN', "Laplacian",
+ "Use laplacian method for relaxation")
+ ],
+ default='HC'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_uv_sculpt_enabled
+ del scene.muv_uv_sculpt_enable
+ del scene.muv_uv_sculpt_radius
+ del scene.muv_uv_sculpt_strength
+ del scene.muv_uv_sculpt_tools
+ del scene.muv_uv_sculpt_show_brush
+ del scene.muv_uv_sculpt_pinch_invert
+ del scene.muv_uv_sculpt_relax_method
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_UVSculpt(bpy.types.Operator):
"""
- Operation class: Render Brush
+ Operation class: UV Sculpt in View3D
"""
- bl_idname = "uv.muv_uvsculpt_renderer"
- bl_label = "Brush Renderer"
- bl_description = "Brush Renderer in View3D"
+ bl_idname = "uv.muv_uv_sculpt_operator"
+ bl_label = "UV Sculpt"
+ bl_description = "UV Sculpt in View3D"
+ bl_options = {'REGISTER'}
__handle = None
-
- @staticmethod
- def handle_add(obj, context):
- if MUV_UVSculptRenderer.__handle is None:
+ __timer = None
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ return is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return 1 if cls.__handle else 0
+
+ @classmethod
+ def handle_add(cls, obj, context):
+ if not cls.__handle:
sv = bpy.types.SpaceView3D
- MUV_UVSculptRenderer.__handle = sv.draw_handler_add(
- MUV_UVSculptRenderer.draw_brush,
- (obj, context), "WINDOW", "POST_PIXEL")
+ cls.__handle = sv.draw_handler_add(cls.draw_brush, (obj, context),
+ "WINDOW", "POST_PIXEL")
+ if not cls.__timer:
+ cls.__timer = context.window_manager.event_timer_add(
+ 0.1, context.window)
+ context.window_manager.modal_handler_add(obj)
- @staticmethod
- def handle_remove():
- if MUV_UVSculptRenderer.__handle is not None:
+ @classmethod
+ def handle_remove(cls, context):
+ if cls.__handle:
sv = bpy.types.SpaceView3D
- sv.draw_handler_remove(
- MUV_UVSculptRenderer.__handle, "WINDOW")
- MUV_UVSculptRenderer.__handle = None
-
- @staticmethod
- def draw_brush(obj, context):
+ sv.draw_handler_remove(cls.__handle, "WINDOW")
+ cls.__handle = None
+ if cls.__timer:
+ context.window_manager.event_timer_remove(cls.__timer)
+ cls.__timer = None
+
+ @classmethod
+ def draw_brush(cls, obj, context):
sc = context.scene
prefs = context.user_preferences.addons["uv_magic_uv"].preferences
@@ -72,12 +211,12 @@ class MUV_UVSculptRenderer(bpy.types.Operator):
theta = 2 * pi / num_segment
fact_t = tan(theta)
fact_r = cos(theta)
- color = prefs.uvsculpt_brush_color
+ color = prefs.uv_sculpt_brush_color
bgl.glBegin(bgl.GL_LINE_STRIP)
bgl.glColor4f(color[0], color[1], color[2], color[3])
- x = sc.muv_uvsculpt_radius * cos(0.0)
- y = sc.muv_uvsculpt_radius * sin(0.0)
+ x = sc.muv_uv_sculpt_radius * cos(0.0)
+ y = sc.muv_uv_sculpt_radius * sin(0.0)
for _ in range(num_segment):
bgl.glVertex2f(x + obj.current_mco.x, y + obj.current_mco.y)
tx = -y
@@ -88,19 +227,7 @@ class MUV_UVSculptRenderer(bpy.types.Operator):
y = y * fact_r
bgl.glEnd()
-
-class MUV_UVSculptOps(bpy.types.Operator):
- """
- Operation class: UV Sculpt in View3D
- """
-
- bl_idname = "uv.muv_uvsculpt_ops"
- bl_label = "UV Sculpt"
- bl_description = "UV Sculpt in View3D"
- bl_options = {'REGISTER'}
-
def __init__(self):
- self.__timer = None
self.__loop_info = []
self.__stroking = False
self.current_mco = Vector((0.0, 0.0))
@@ -137,7 +264,7 @@ class MUV_UVSculptOps(bpy.types.Operator):
loc_2d = view3d_utils.location_3d_to_region_2d(
region, space.region_3d, world_mat * l.vert.co)
diff = loc_2d - self.__initial_mco
- if diff.length < sc.muv_uvsculpt_radius:
+ if diff.length < sc.muv_uv_sculpt_radius:
info = {
"face_idx": f.index,
"loop_idx": i,
@@ -145,8 +272,8 @@ class MUV_UVSculptOps(bpy.types.Operator):
"initial_vco_2d": loc_2d,
"initial_uv": l[uv_layer].uv.copy(),
"strength": self.__get_strength(
- diff.length, sc.muv_uvsculpt_radius,
- sc.muv_uvsculpt_strength)
+ diff.length, sc.muv_uv_sculpt_radius,
+ sc.muv_uv_sculpt_strength)
}
self.__loop_info.append(info)
@@ -158,13 +285,13 @@ class MUV_UVSculptOps(bpy.types.Operator):
uv_layer = bm.loops.layers.uv.verify()
mco = self.current_mco
- if sc.muv_uvsculpt_tools == 'GRAB':
+ if sc.muv_uv_sculpt_tools == 'GRAB':
for info in self.__loop_info:
diff_uv = (mco - self.__initial_mco) * info["strength"]
l = bm.faces[info["face_idx"]].loops[info["loop_idx"]]
l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0
- elif sc.muv_uvsculpt_tools == 'PINCH':
+ elif sc.muv_uv_sculpt_tools == 'PINCH':
_, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
loop_info = []
for f in bm.faces:
@@ -174,7 +301,7 @@ class MUV_UVSculptOps(bpy.types.Operator):
loc_2d = view3d_utils.location_3d_to_region_2d(
region, space.region_3d, world_mat * l.vert.co)
diff = loc_2d - self.__initial_mco
- if diff.length < sc.muv_uvsculpt_radius:
+ if diff.length < sc.muv_uv_sculpt_radius:
info = {
"face_idx": f.index,
"loop_idx": i,
@@ -182,8 +309,8 @@ class MUV_UVSculptOps(bpy.types.Operator):
"initial_vco_2d": loc_2d,
"initial_uv": l[uv_layer].uv.copy(),
"strength": self.__get_strength(
- diff.length, sc.muv_uvsculpt_radius,
- sc.muv_uvsculpt_strength)
+ diff.length, sc.muv_uv_sculpt_radius,
+ sc.muv_uv_sculpt_strength)
}
loop_info.append(info)
@@ -215,13 +342,13 @@ class MUV_UVSculptOps(bpy.types.Operator):
# move to target UV coordinate
for info in loop_info:
l = bm.faces[info["face_idx"]].loops[info["loop_idx"]]
- if sc.muv_uvsculpt_pinch_invert:
+ if sc.muv_uv_sculpt_pinch_invert:
diff_uv = (l[uv_layer].uv - target_uv) * info["strength"]
else:
diff_uv = (target_uv - l[uv_layer].uv) * info["strength"]
l[uv_layer].uv = l[uv_layer].uv + diff_uv / 10.0
- elif sc.muv_uvsculpt_tools == 'RELAX':
+ elif sc.muv_uv_sculpt_tools == 'RELAX':
_, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
# get vertex and loop relation
@@ -265,19 +392,19 @@ class MUV_UVSculptOps(bpy.types.Operator):
loc_2d = view3d_utils.location_3d_to_region_2d(
region, space.region_3d, world_mat * l.vert.co)
diff = loc_2d - self.__initial_mco
- if diff.length >= sc.muv_uvsculpt_radius:
+ if diff.length >= sc.muv_uv_sculpt_radius:
continue
db = vert_db[l.vert]
strength = self.__get_strength(diff.length,
- sc.muv_uvsculpt_radius,
- sc.muv_uvsculpt_strength)
+ sc.muv_uv_sculpt_radius,
+ sc.muv_uv_sculpt_strength)
base = (1.0 - strength) * l[uv_layer].uv
- if sc.muv_uvsculpt_relax_method == 'HC':
+ if sc.muv_uv_sculpt_relax_method == 'HC':
t = 0.5 * (db["uv_b"] + db["uv_sum_b"] / d["uv_count"])
diff = strength * (db["uv_p"] - t)
target_uv = base + diff
- elif sc.muv_uvsculpt_relax_method == 'LAPLACIAN':
+ elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN':
diff = strength * db["uv_p"]
target_uv = base + diff
else:
@@ -294,7 +421,7 @@ class MUV_UVSculptOps(bpy.types.Operator):
uv_layer = bm.loops.layers.uv.verify()
mco = self.current_mco
- if sc.muv_uvsculpt_tools == 'GRAB':
+ if sc.muv_uv_sculpt_tools == 'GRAB':
for info in self.__loop_info:
diff_uv = (mco - self.__initial_mco) * info["strength"]
l = bm.faces[info["face_idx"]].loops[info["loop_idx"]]
@@ -303,23 +430,24 @@ class MUV_UVSculptOps(bpy.types.Operator):
bmesh.update_edit_mesh(obj.data)
def modal(self, context, event):
- props = context.scene.muv_props.uvsculpt
-
if context.area:
context.area.tag_redraw()
- if not props.running:
- if self.__timer is not None:
- MUV_UVSculptRenderer.handle_remove()
- context.window_manager.event_timer_remove(self.__timer)
- self.__timer = None
+ if not MUV_OT_UVSculpt.is_running(context):
+ MUV_OT_UVSculpt.handle_remove(context)
+
return {'FINISHED'}
self.current_mco = Vector((event.mouse_region_x, event.mouse_region_y))
- area, _, _ = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
- if self.current_mco.x < 0 or self.current_mco.x > area.width or \
- self.current_mco.y < 0 or self.current_mco.y > area.height:
+ region_types = [
+ 'HEADER',
+ 'UI',
+ 'TOOLS',
+ 'TOOL_PROPS',
+ ]
+ if not common.mouse_on_area(event, 'VIEW_3D') or \
+ common.mouse_on_regions(event, 'VIEW_3D', region_types):
return {'PASS_THROUGH'}
if event.type == 'LEFTMOUSE':
@@ -331,30 +459,25 @@ class MUV_UVSculptOps(bpy.types.Operator):
if self.__stroking:
self.__stroke_exit(context, event)
self.__stroking = False
+ return {'RUNNING_MODAL'}
elif event.type == 'MOUSEMOVE':
if self.__stroking:
self.__stroke_apply(context, event)
+ return {'RUNNING_MODAL'}
elif event.type == 'TIMER':
if self.__stroking:
self.__stroke_apply(context, event)
+ return {'RUNNING_MODAL'}
- return {'RUNNING_MODAL'}
+ return {'PASS_THROUGH'}
def invoke(self, context, _):
- props = context.scene.muv_props.uvsculpt
-
if context.area:
context.area.tag_redraw()
- if props.running:
- props.running = False
- return {'FINISHED'}
-
- props.running = True
- 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_UVSculptRenderer.handle_add(self, context)
+ if MUV_OT_UVSculpt.is_running(context):
+ MUV_OT_UVSculpt.handle_remove(context)
+ else:
+ MUV_OT_UVSculpt.handle_add(self, context)
return {'RUNNING_MODAL'}
diff --git a/uv_magic_uv/legacy/op/uvw.py b/uv_magic_uv/legacy/op/uvw.py
new file mode 100644
index 00000000..56777b18
--- /dev/null
+++ b/uv_magic_uv/legacy/op/uvw.py
@@ -0,0 +1,181 @@
+# <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__ = "Alexander Milovsky, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+import bmesh
+from bpy.props import (
+ FloatProperty,
+ FloatVectorProperty,
+ BoolProperty
+)
+
+from ... import common
+from ...impl import uvw_impl as impl
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_UVW_BoxMap',
+ 'MUV_OT_UVW_BestPlanerMap',
+]
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "uvw"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_uvw_enabled = BoolProperty(
+ name="UVW Enabled",
+ description="UVW is enabled",
+ default=False
+ )
+ scene.muv_uvw_assign_uvmap = BoolProperty(
+ name="Assign UVMap",
+ description="Assign UVMap when no UVmaps are available",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_uvw_enabled
+ del scene.muv_uvw_assign_uvmap
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_UVW_BoxMap(bpy.types.Operator):
+ bl_idname = "uv.muv_uvw_operator_box_map"
+ bl_label = "Box Map"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ size = FloatProperty(
+ name="Size",
+ default=1.0,
+ precision=4
+ )
+ rotation = FloatVectorProperty(
+ name="XYZ Rotation",
+ size=3,
+ default=(0.0, 0.0, 0.0)
+ )
+ offset = FloatVectorProperty(
+ name="XYZ Offset",
+ size=3,
+ default=(0.0, 0.0, 0.0)
+ )
+ tex_aspect = FloatProperty(
+ name="Texture Aspect",
+ default=1.0,
+ precision=4
+ )
+ assign_uvmap = BoolProperty(
+ name="Assign UVMap",
+ description="Assign UVMap when no UVmaps are available",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # get UV layer
+ uv_layer = impl.get_uv_layer(self, bm, self.assign_uvmap)
+ if not uv_layer:
+ return {'CANCELLED'}
+
+ impl.apply_box_map(bm, uv_layer, self.size, self.offset,
+ self.rotation, self.tex_aspect)
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_UVW_BestPlanerMap(bpy.types.Operator):
+ bl_idname = "uv.muv_uvw_operator_best_planer_map"
+ bl_label = "Best Planer Map"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ size = FloatProperty(
+ name="Size",
+ default=1.0,
+ precision=4
+ )
+ rotation = FloatProperty(
+ name="XY Rotation",
+ default=0.0
+ )
+ offset = FloatVectorProperty(
+ name="XY Offset",
+ size=2,
+ default=(0.0, 0.0)
+ )
+ tex_aspect = FloatProperty(
+ name="Texture Aspect",
+ default=1.0,
+ precision=4
+ )
+ assign_uvmap = BoolProperty(
+ name="Assign UVMap",
+ description="Assign UVMap when no UVmaps are available",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # get UV layer
+ uv_layer = impl.get_uv_layer(self, bm, self.assign_uvmap)
+ if not uv_layer:
+ return {'CANCELLED'}
+
+ impl.apply_planer_map(bm, uv_layer, self.size, self.offset,
+ self.rotation, self.tex_aspect)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/legacy/op/world_scale_uv.py b/uv_magic_uv/legacy/op/world_scale_uv.py
new file mode 100644
index 00000000..e56b6bfa
--- /dev/null
+++ b/uv_magic_uv/legacy/op/world_scale_uv.py
@@ -0,0 +1,655 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from math import sqrt
+
+import bpy
+import bmesh
+from mathutils import Vector
+from bpy.props import (
+ EnumProperty,
+ FloatProperty,
+ IntVectorProperty,
+ BoolProperty,
+)
+
+from ... import common
+from ...utils.bl_class_registry import BlClassRegistry
+from ...utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_WorldScaleUV_Measure',
+ 'MUV_OT_WorldScaleUV_ApplyManual',
+ 'MUV_OT_WorldScaleUV_ApplyScalingDensity',
+ 'MUV_OT_WorldScaleUV_ApplyProportionalToMesh',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def measure_wsuv_info(obj, tex_size=None):
+ mesh_area = common.measure_mesh_area(obj)
+ uv_area = common.measure_uv_area(obj, tex_size)
+
+ if not uv_area:
+ return None, mesh_area, None
+
+ if mesh_area == 0.0:
+ density = 0.0
+ else:
+ density = sqrt(uv_area) / sqrt(mesh_area)
+
+ return uv_area, mesh_area, density
+
+
+@PropertyClassRegistry(legacy=True)
+class Properties:
+ idname = "world_scale_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_world_scale_uv_enabled = BoolProperty(
+ name="World Scale UV Enabled",
+ description="World Scale UV is enabled",
+ default=False
+ )
+ scene.muv_world_scale_uv_src_mesh_area = FloatProperty(
+ name="Mesh Area",
+ description="Source Mesh Area",
+ default=0.0,
+ min=0.0
+ )
+ scene.muv_world_scale_uv_src_uv_area = FloatProperty(
+ name="UV Area",
+ description="Source UV Area",
+ default=0.0,
+ min=0.0
+ )
+ scene.muv_world_scale_uv_src_density = FloatProperty(
+ name="Density",
+ description="Source Texel Density",
+ default=0.0,
+ min=0.0
+ )
+ scene.muv_world_scale_uv_tgt_density = FloatProperty(
+ name="Density",
+ description="Target Texel Density",
+ default=0.0,
+ min=0.0
+ )
+ scene.muv_world_scale_uv_tgt_scaling_factor = FloatProperty(
+ name="Scaling Factor",
+ default=1.0,
+ max=1000.0,
+ min=0.00001
+ )
+ scene.muv_world_scale_uv_tgt_texture_size = IntVectorProperty(
+ name="Texture Size",
+ size=2,
+ min=1,
+ soft_max=10240,
+ default=(1024, 1024),
+ )
+ scene.muv_world_scale_uv_mode = EnumProperty(
+ name="Mode",
+ description="Density calculation mode",
+ items=[
+ ('PROPORTIONAL_TO_MESH', "Proportional to Mesh",
+ "Apply density proportionaled by mesh size"),
+ ('SCALING_DENSITY', "Scaling Density",
+ "Apply scaled density from source"),
+ ('SAME_DENSITY', "Same Density",
+ "Apply same density of source"),
+ ('MANUAL', "Manual", "Specify density and size by manual"),
+ ],
+ default='MANUAL'
+ )
+ scene.muv_world_scale_uv_origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', "Center", "Center"),
+ ('LEFT_TOP', "Left Top", "Left Bottom"),
+ ('LEFT_CENTER', "Left Center", "Left Center"),
+ ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"),
+ ('CENTER_TOP', "Center Top", "Center Top"),
+ ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"),
+ ('RIGHT_TOP', "Right Top", "Right Top"),
+ ('RIGHT_CENTER', "Right Center", "Right Center"),
+ ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom")
+
+ ],
+ default='CENTER'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_world_scale_uv_enabled
+ del scene.muv_world_scale_uv_src_mesh_area
+ del scene.muv_world_scale_uv_src_uv_area
+ del scene.muv_world_scale_uv_src_density
+ del scene.muv_world_scale_uv_tgt_density
+ del scene.muv_world_scale_uv_tgt_scaling_factor
+ del scene.muv_world_scale_uv_mode
+ del scene.muv_world_scale_uv_origin
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_WorldScaleUV_Measure(bpy.types.Operator):
+ """
+ Operation class: Measure face size
+ """
+
+ bl_idname = "uv.muv_world_scale_uv_operator_measure"
+ bl_label = "Measure World Scale UV"
+ bl_description = "Measure face size for scale calculation"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
+ def execute(self, context):
+ sc = context.scene
+ obj = context.active_object
+
+ uv_area, mesh_area, density = measure_wsuv_info(obj)
+ if not uv_area:
+ self.report({'WARNING'},
+ "Object must have more than one UV map and texture")
+ return {'CANCELLED'}
+
+ sc.muv_world_scale_uv_src_uv_area = uv_area
+ sc.muv_world_scale_uv_src_mesh_area = mesh_area
+ sc.muv_world_scale_uv_src_density = density
+
+ self.report({'INFO'},
+ "UV Area: {0}, Mesh Area: {1}, Texel Density: {2}"
+ .format(uv_area, mesh_area, density))
+
+ return {'FINISHED'}
+
+
+def apply(obj, origin, factor):
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ sel_faces = [f for f in bm.faces if f.select]
+
+ uv_layer = bm.loops.layers.uv.verify()
+
+ # calculate origin
+ if origin == 'CENTER':
+ origin = Vector((0.0, 0.0))
+ num = 0
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin = origin + uv
+ num = num + 1
+ origin = origin / num
+ elif origin == 'LEFT_TOP':
+ origin = Vector((100000.0, -100000.0))
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = min(origin.x, uv.x)
+ origin.y = max(origin.y, uv.y)
+ elif origin == 'LEFT_CENTER':
+ origin = Vector((100000.0, 0.0))
+ num = 0
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = min(origin.x, uv.x)
+ origin.y = origin.y + uv.y
+ num = num + 1
+ origin.y = origin.y / num
+ elif origin == 'LEFT_BOTTOM':
+ origin = Vector((100000.0, 100000.0))
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = min(origin.x, uv.x)
+ origin.y = min(origin.y, uv.y)
+ elif origin == 'CENTER_TOP':
+ origin = Vector((0.0, -100000.0))
+ num = 0
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = origin.x + uv.x
+ origin.y = max(origin.y, uv.y)
+ num = num + 1
+ origin.x = origin.x / num
+ elif origin == 'CENTER_BOTTOM':
+ origin = Vector((0.0, 100000.0))
+ num = 0
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = origin.x + uv.x
+ origin.y = min(origin.y, uv.y)
+ num = num + 1
+ origin.x = origin.x / num
+ elif origin == 'RIGHT_TOP':
+ origin = Vector((-100000.0, -100000.0))
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = max(origin.x, uv.x)
+ origin.y = max(origin.y, uv.y)
+ elif origin == 'RIGHT_CENTER':
+ origin = Vector((-100000.0, 0.0))
+ num = 0
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = max(origin.x, uv.x)
+ origin.y = origin.y + uv.y
+ num = num + 1
+ origin.y = origin.y / num
+ elif origin == 'RIGHT_BOTTOM':
+ origin = Vector((-100000.0, 100000.0))
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = max(origin.x, uv.x)
+ origin.y = min(origin.y, uv.y)
+
+ # update UV coordinate
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ diff = uv - origin
+ l[uv_layer].uv = origin + diff * factor
+
+ bmesh.update_edit_mesh(obj.data)
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator):
+ """
+ Operation class: Apply scaled UV (Manual)
+ """
+
+ bl_idname = "uv.muv_world_scale_uv_operator_apply_manual"
+ bl_label = "Apply World Scale UV (Manual)"
+ bl_description = "Apply scaled UV based on user specification"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ tgt_density = FloatProperty(
+ name="Density",
+ description="Target Texel Density",
+ default=1.0,
+ min=0.0
+ )
+ tgt_texture_size = IntVectorProperty(
+ name="Texture Size",
+ size=2,
+ min=1,
+ soft_max=10240,
+ default=(1024, 1024),
+ )
+ origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', "Center", "Center"),
+ ('LEFT_TOP', "Left Top", "Left Bottom"),
+ ('LEFT_CENTER', "Left Center", "Left Center"),
+ ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"),
+ ('CENTER_TOP', "Center Top", "Center Top"),
+ ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"),
+ ('RIGHT_TOP', "Right Top", "Right Top"),
+ ('RIGHT_CENTER', "Right Center", "Right Center"),
+ ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom")
+
+ ],
+ default='CENTER'
+ )
+ show_dialog = BoolProperty(
+ name="Show Diaglog Menu",
+ description="Show dialog menu if true",
+ default=True,
+ options={'HIDDEN', 'SKIP_SAVE'}
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
+ def __apply_manual(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ tex_size = self.tgt_texture_size
+ uv_area, _, density = measure_wsuv_info(obj, tex_size)
+ if not uv_area:
+ self.report({'WARNING'},
+ "Object must have more than one UV map")
+ return {'CANCELLED'}
+
+ tgt_density = self.tgt_density
+ factor = tgt_density / density
+
+ apply(context.active_object, self.origin, factor)
+ self.report({'INFO'}, "Scaling factor: {0}".format(factor))
+
+ return {'FINISHED'}
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.prop(self, "tgt_density")
+ layout.prop(self, "tgt_texture_size")
+ layout.prop(self, "origin")
+
+ layout.separator()
+
+ def invoke(self, context, _):
+ if self.show_dialog:
+ wm = context.window_manager
+ return wm.invoke_props_dialog(self)
+
+ return self.execute(context)
+
+ def execute(self, context):
+ return self.__apply_manual(context)
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator):
+ """
+ Operation class: Apply scaled UV (Scaling Density)
+ """
+
+ bl_idname = "uv.muv_world_scale_uv_operator_apply_scaling_density"
+ bl_label = "Apply World Scale UV (Scaling Density)"
+ bl_description = "Apply scaled UV with scaling density"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ tgt_scaling_factor = FloatProperty(
+ name="Scaling Factor",
+ default=1.0,
+ max=1000.0,
+ min=0.00001
+ )
+ origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', "Center", "Center"),
+ ('LEFT_TOP', "Left Top", "Left Bottom"),
+ ('LEFT_CENTER', "Left Center", "Left Center"),
+ ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"),
+ ('CENTER_TOP', "Center Top", "Center Top"),
+ ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"),
+ ('RIGHT_TOP', "Right Top", "Right Top"),
+ ('RIGHT_CENTER', "Right Center", "Right Center"),
+ ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom")
+
+ ],
+ default='CENTER'
+ )
+ src_density = FloatProperty(
+ name="Density",
+ description="Source Texel Density",
+ default=0.0,
+ min=0.0,
+ options={'HIDDEN'}
+ )
+ same_density = BoolProperty(
+ name="Same Density",
+ description="Apply same density",
+ default=False,
+ options={'HIDDEN'}
+ )
+ show_dialog = BoolProperty(
+ name="Show Diaglog Menu",
+ description="Show dialog menu if true",
+ default=True,
+ options={'HIDDEN', 'SKIP_SAVE'}
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
+ def __apply_scaling_density(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ uv_area, _, density = measure_wsuv_info(obj)
+ if not uv_area:
+ self.report({'WARNING'},
+ "Object must have more than one UV map and texture")
+ return {'CANCELLED'}
+
+ tgt_density = self.src_density * self.tgt_scaling_factor
+ factor = tgt_density / density
+
+ apply(context.active_object, self.origin, factor)
+ self.report({'INFO'}, "Scaling factor: {0}".format(factor))
+
+ return {'FINISHED'}
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.label("Source:")
+ col = layout.column()
+ col.prop(self, "src_density")
+ col.enabled = False
+
+ layout.separator()
+
+ if not self.same_density:
+ layout.prop(self, "tgt_scaling_factor")
+ layout.prop(self, "origin")
+
+ layout.separator()
+
+ def invoke(self, context, _):
+ sc = context.scene
+
+ if self.show_dialog:
+ wm = context.window_manager
+
+ if self.same_density:
+ self.tgt_scaling_factor = 1.0
+ else:
+ self.tgt_scaling_factor = \
+ sc.muv_world_scale_uv_tgt_scaling_factor
+ self.src_density = sc.muv_world_scale_uv_src_density
+
+ return wm.invoke_props_dialog(self)
+
+ return self.execute(context)
+
+ def execute(self, context):
+ if self.same_density:
+ self.tgt_scaling_factor = 1.0
+
+ return self.__apply_scaling_density(context)
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator):
+ """
+ Operation class: Apply scaled UV (Proportional to mesh)
+ """
+
+ bl_idname = "uv.muv_world_scale_uv_operator_apply_proportional_to_mesh"
+ bl_label = "Apply World Scale UV (Proportional to mesh)"
+ bl_description = "Apply scaled UV proportionaled to mesh"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', "Center", "Center"),
+ ('LEFT_TOP', "Left Top", "Left Bottom"),
+ ('LEFT_CENTER', "Left Center", "Left Center"),
+ ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"),
+ ('CENTER_TOP', "Center Top", "Center Top"),
+ ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"),
+ ('RIGHT_TOP', "Right Top", "Right Top"),
+ ('RIGHT_CENTER', "Right Center", "Right Center"),
+ ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom")
+
+ ],
+ default='CENTER'
+ )
+ src_density = FloatProperty(
+ name="Source Density",
+ description="Source Texel Density",
+ default=0.0,
+ min=0.0,
+ options={'HIDDEN'}
+ )
+ src_uv_area = FloatProperty(
+ name="Source UV Area",
+ description="Source UV Area",
+ default=0.0,
+ min=0.0,
+ options={'HIDDEN'}
+ )
+ src_mesh_area = FloatProperty(
+ name="Source Mesh Area",
+ description="Source Mesh Area",
+ default=0.0,
+ min=0.0,
+ options={'HIDDEN'}
+ )
+ show_dialog = BoolProperty(
+ name="Show Diaglog Menu",
+ description="Show dialog menu if true",
+ default=True,
+ options={'HIDDEN', 'SKIP_SAVE'}
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
+
+ def __apply_proportional_to_mesh(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ uv_area, mesh_area, density = measure_wsuv_info(obj)
+ if not uv_area:
+ self.report({'WARNING'},
+ "Object must have more than one UV map and texture")
+ return {'CANCELLED'}
+
+ tgt_density = self.src_density * sqrt(mesh_area) / sqrt(
+ self.src_mesh_area)
+
+ factor = tgt_density / density
+
+ apply(context.active_object, self.origin, factor)
+ self.report({'INFO'}, "Scaling factor: {0}".format(factor))
+
+ return {'FINISHED'}
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.label("Source:")
+ col = layout.column(align=True)
+ col.prop(self, "src_density")
+ col.prop(self, "src_uv_area")
+ col.prop(self, "src_mesh_area")
+ col.enabled = False
+
+ layout.separator()
+ layout.prop(self, "origin")
+
+ layout.separator()
+
+ def invoke(self, context, _):
+ if self.show_dialog:
+ wm = context.window_manager
+ sc = context.scene
+
+ self.src_density = sc.muv_world_scale_uv_src_density
+ self.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area
+
+ return wm.invoke_props_dialog(self)
+
+ return self.execute(context)
+
+ def execute(self, context):
+ return self.__apply_proportional_to_mesh(context)
diff --git a/uv_magic_uv/legacy/preferences.py b/uv_magic_uv/legacy/preferences.py
new file mode 100644
index 00000000..931cc1d4
--- /dev/null
+++ b/uv_magic_uv/legacy/preferences.py
@@ -0,0 +1,468 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import (
+ FloatProperty,
+ FloatVectorProperty,
+ BoolProperty,
+ EnumProperty,
+ IntProperty,
+)
+from bpy.types import AddonPreferences
+
+from . import op
+from . import ui
+from .. import addon_updater_ops
+from ..utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'add_builtin_menu',
+ 'remove_builtin_menu',
+ 'Preferences'
+]
+
+
+def view3d_uvmap_menu_fn(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.separator()
+ layout.label(text="Copy/Paste UV", icon='IMAGE_COL')
+ # Copy/Paste UV
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_CopyPasteUV.bl_idname,
+ text="Copy/Paste UV")
+ # Transfer UV
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TransferUV.bl_idname,
+ text="Transfer UV")
+
+ layout.separator()
+ layout.label("UV Manipulation", icon='IMAGE_COL')
+ # Flip/Rotate UV
+ ops = layout.operator(op.flip_rotate_uv.MUV_OT_FlipRotate.bl_idname,
+ text="Flip/Rotate UV")
+ ops.seams = sc.muv_flip_rotate_uv_seams
+ # Mirror UV
+ ops = layout.operator(op.mirror_uv.MUV_OT_MirrorUV.bl_idname,
+ text="Mirror UV")
+ ops.axis = sc.muv_mirror_uv_axis
+ # Move UV
+ layout.operator(op.move_uv.MUV_OT_MoveUV.bl_idname, text="Move UV")
+ # World Scale UV
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_WorldScaleUV.bl_idname,
+ text="World Scale UV")
+ # Preserve UV
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_PreserveUVAspect.bl_idname,
+ text="Preserve UV")
+ # Texture Lock
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TextureLock.bl_idname,
+ text="Texture Lock")
+ # Texture Wrap
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TextureWrap.bl_idname,
+ text="Texture Wrap")
+ # UV Sculpt
+ layout.prop(sc, "muv_uv_sculpt_enable", text="UV Sculpt")
+
+ layout.separator()
+ layout.label("UV Mapping", icon='IMAGE_COL')
+ # Unwrap Constraint
+ ops = layout.operator(
+ op.unwrap_constraint.MUV_OT_UnwrapConstraint.bl_idname,
+ text="Unwrap Constraint")
+ ops.u_const = sc.muv_unwrap_constraint_u_const
+ ops.v_const = sc.muv_unwrap_constraint_v_const
+ # Texture Projection
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TextureProjection.bl_idname,
+ text="Texture Projection")
+ # UVW
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_UVW.bl_idname, text="UVW")
+
+
+def view3d_object_menu_fn(self, _):
+ layout = self.layout
+
+ layout.separator()
+ # Copy/Paste UV (Among Objecct)
+ layout.menu(ui.VIEW3D_MT_object.MUV_MT_CopyPasteUV_Object.bl_idname,
+ text="Copy/Paste UV")
+ layout.label("Copy/Paste UV", icon='IMAGE_COL')
+
+
+def image_uvs_menu_fn(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.separator()
+ # Align UV Cursor
+ layout.menu(ui.IMAGE_MT_uvs.MUV_MT_AlignUVCursor.bl_idname,
+ text="Align UV Cursor")
+ # UV Bounding Box
+ layout.prop(sc, "muv_uv_bounding_box_show", text="UV Bounding Box")
+ # UV Inspection
+ layout.menu(ui.IMAGE_MT_uvs.MUV_MT_UVInspection.bl_idname,
+ text="UV Inspection")
+ layout.label("Editor Enhancement", icon='IMAGE_COL')
+
+ layout.separator()
+ # Align UV
+ layout.menu(ui.IMAGE_MT_uvs.MUV_MT_AlignUV.bl_idname, text="Align UV")
+ # Smooth UV
+ ops = layout.operator(op.smooth_uv.MUV_OT_SmoothUV.bl_idname,
+ text="Smooth")
+ ops.transmission = sc.muv_smooth_uv_transmission
+ ops.select = sc.muv_smooth_uv_select
+ ops.mesh_infl = sc.muv_smooth_uv_mesh_infl
+ # Select UV
+ layout.menu(ui.IMAGE_MT_uvs.MUV_MT_SelectUV.bl_idname, text="Select UV")
+ # Pack UV
+ ops = layout.operator(op.pack_uv.MUV_OT_PackUV.bl_idname, text="Pack UV")
+ ops.allowable_center_deviation = sc.muv_pack_uv_allowable_center_deviation
+ ops.allowable_size_deviation = sc.muv_pack_uv_allowable_size_deviation
+ layout.label("UV Manipulation", icon='IMAGE_COL')
+
+ layout.separator()
+ # Copy/Paste UV (on UV/Image Editor)
+ layout.menu(ui.IMAGE_MT_uvs.MUV_MT_CopyPasteUV_UVEdit.bl_idname,
+ text="Copy/Paste UV")
+ layout.label("Copy/Paste UV", icon='IMAGE_COL')
+
+
+def add_builtin_menu():
+ bpy.types.VIEW3D_MT_uv_map.append(view3d_uvmap_menu_fn)
+ bpy.types.VIEW3D_MT_object.append(view3d_object_menu_fn)
+ bpy.types.IMAGE_MT_uvs.append(image_uvs_menu_fn)
+
+
+def remove_builtin_menu():
+ bpy.types.IMAGE_MT_uvs.remove(image_uvs_menu_fn)
+ bpy.types.VIEW3D_MT_uv_map.remove(view3d_uvmap_menu_fn)
+ bpy.types.VIEW3D_MT_object.remove(view3d_object_menu_fn)
+
+
+@BlClassRegistry(legacy=True)
+class Preferences(AddonPreferences):
+ """Preferences class: Preferences for this add-on"""
+
+ bl_idname = "uv_magic_uv"
+
+ def update_enable_builtin_menu(self, _):
+ if self['enable_builtin_menu']:
+ add_builtin_menu()
+ else:
+ remove_builtin_menu()
+
+ # enable to add features to built-in menu
+ enable_builtin_menu = BoolProperty(
+ name="Built-in Menu",
+ description="Enable built-in menu",
+ default=True,
+ update=update_enable_builtin_menu
+ )
+
+ # for UV Sculpt
+ uv_sculpt_brush_color = FloatVectorProperty(
+ name="Color",
+ description="Color",
+ default=(1.0, 0.4, 0.4, 1.0),
+ min=0.0,
+ max=1.0,
+ size=4,
+ subtype='COLOR'
+ )
+
+ # for Overlapped UV
+ uv_inspection_overlapped_color = FloatVectorProperty(
+ name="Color",
+ description="Color",
+ default=(0.0, 0.0, 1.0, 0.3),
+ min=0.0,
+ max=1.0,
+ size=4,
+ subtype='COLOR'
+ )
+
+ # for Flipped UV
+ uv_inspection_flipped_color = FloatVectorProperty(
+ name="Color",
+ description="Color",
+ default=(1.0, 0.0, 0.0, 0.3),
+ min=0.0,
+ max=1.0,
+ size=4,
+ subtype='COLOR'
+ )
+
+ # for Texture Projection
+ texture_projection_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
+ uv_bounding_box_cp_size = FloatProperty(
+ name="Size",
+ description="Control Point Size",
+ default=6.0,
+ min=3.0,
+ max=100.0)
+ uv_bounding_box_cp_react_size = FloatProperty(
+ name="React Size",
+ description="Size event fired",
+ default=10.0,
+ min=3.0,
+ max=100.0)
+
+ # for UI
+ category = EnumProperty(
+ name="Category",
+ description="Preferences Category",
+ items=[
+ ('INFO', "Information", "Information about this add-on"),
+ ('CONFIG', "Configuration", "Configuration about this add-on"),
+ ('UPDATE', "Update", "Update this add-on"),
+ ],
+ default='INFO'
+ )
+ info_desc_expanded = BoolProperty(
+ name="Description",
+ description="Description",
+ default=False
+ )
+ info_loc_expanded = BoolProperty(
+ name="Location",
+ description="Location",
+ default=False
+ )
+ conf_uv_sculpt_expanded = BoolProperty(
+ name="UV Sculpt",
+ description="UV Sculpt",
+ default=False
+ )
+ conf_uv_inspection_expanded = BoolProperty(
+ name="UV Inspection",
+ description="UV Inspection",
+ default=False
+ )
+ conf_texture_projection_expanded = BoolProperty(
+ name="Texture Projection",
+ description="Texture Projection",
+ default=False
+ )
+ conf_uv_bounding_box_expanded = BoolProperty(
+ name="UV Bounding Box",
+ description="UV Bounding Box",
+ default=False
+ )
+
+ # for add-on updater
+ auto_check_update = BoolProperty(
+ name="Auto-check for Update",
+ description="If enabled, auto-check for updates using an interval",
+ default=False
+ )
+ updater_intrval_months = IntProperty(
+ name='Months',
+ description="Number of months between checking for updates",
+ default=0,
+ min=0
+ )
+ updater_intrval_days = IntProperty(
+ name='Days',
+ description="Number of days between checking for updates",
+ default=7,
+ min=0
+ )
+ updater_intrval_hours = IntProperty(
+ name='Hours',
+ description="Number of hours between checking for updates",
+ default=0,
+ min=0,
+ max=23
+ )
+ updater_intrval_minutes = IntProperty(
+ name='Minutes',
+ description="Number of minutes between checking for updates",
+ default=0,
+ min=0,
+ max=59
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.row().prop(self, "category", expand=True)
+
+ if self.category == 'INFO':
+ layout.prop(
+ self, "info_desc_expanded", text="Description",
+ icon='DISCLOSURE_TRI_DOWN' if self.info_desc_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.info_desc_expanded:
+ column = layout.column(align=True)
+ column.label("Magic UV is composed of many UV editing" +
+ " features.")
+ column.label("See tutorial page if you are new to this" +
+ " add-on.")
+ column.label("https://github.com/nutti/Magic-UV/wiki/Tutorial")
+
+ layout.prop(
+ self, "info_loc_expanded", text="Location",
+ icon='DISCLOSURE_TRI_DOWN' if self.info_loc_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.info_loc_expanded:
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("3D View > Tool shelf > Copy/Paste UV (Object mode)")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Copy/Paste UV (Among objects)")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("3D View > Tool shelf > Copy/Paste UV (Edit mode)")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Copy/Paste UV (Among faces in 3D View)")
+ col.label("Transfer UV")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("3D View > Tool shelf > UV Manipulation (Edit mode)")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Flip/Rotate UV")
+ col.label("Mirror UV")
+ col.label("Move UV")
+ col.label("World Scale UV")
+ col.label("Preserve UV Aspect")
+ col.label("Texture Lock")
+ col.label("Texture Wrap")
+ col.label("UV Sculpt")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("3D View > Tool shelf > UV Manipulation (Edit mode)")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Unwrap Constraint")
+ col.label("Texture Projection")
+ col.label("UVW")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("UV/Image Editor > Tool shelf > Copy/Paste UV")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Copy/Paste UV (Among faces in UV/Image Editor)")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("UV/Image Editor > Tool shelf > UV Manipulation")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Align UV")
+ col.label("Smooth UV")
+ col.label("Select UV")
+ col.label("Pack UV (Extension)")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("UV/Image Editor > Tool shelf > Editor Enhancement")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Align UV Cursor")
+ col.label("UV Cursor Location")
+ col.label("UV Bounding Box")
+ col.label("UV Inspection")
+
+ elif self.category == 'CONFIG':
+ layout.prop(self, "enable_builtin_menu", text="Built-in Menu")
+
+ layout.separator()
+
+ layout.prop(
+ self, "conf_uv_sculpt_expanded", text="UV Sculpt",
+ icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_sculpt_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_uv_sculpt_expanded:
+ sp = layout.split(percentage=0.05)
+ col = sp.column() # spacer
+ sp = sp.split(percentage=0.3)
+ col = sp.column()
+ col.label("Brush Color:")
+ col.prop(self, "uv_sculpt_brush_color", text="")
+ layout.separator()
+
+ layout.prop(
+ self, "conf_uv_inspection_expanded", text="UV Inspection",
+ icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_inspection_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_uv_inspection_expanded:
+ sp = layout.split(percentage=0.05)
+ col = sp.column() # spacer
+ sp = sp.split(percentage=0.3)
+ col = sp.column()
+ col.label("Overlapped UV Color:")
+ col.prop(self, "uv_inspection_overlapped_color", text="")
+ sp = sp.split(percentage=0.45)
+ col = sp.column()
+ col.label("Flipped UV Color:")
+ col.prop(self, "uv_inspection_flipped_color", text="")
+ layout.separator()
+
+ layout.prop(
+ self, "conf_texture_projection_expanded",
+ text="Texture Projection",
+ icon='DISCLOSURE_TRI_DOWN'
+ if self.conf_texture_projection_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_texture_projection_expanded:
+ sp = layout.split(percentage=0.05)
+ col = sp.column() # spacer
+ sp = sp.split(percentage=0.3)
+ col = sp.column()
+ col.prop(self, "texture_projection_canvas_padding")
+ layout.separator()
+
+ layout.prop(
+ self, "conf_uv_bounding_box_expanded", text="UV Bounding Box",
+ icon='DISCLOSURE_TRI_DOWN'
+ if self.conf_uv_bounding_box_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_uv_bounding_box_expanded:
+ 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, "uv_bounding_box_cp_size")
+ col.prop(self, "uv_bounding_box_cp_react_size")
+ layout.separator()
+
+ elif self.category == 'UPDATE':
+ addon_updater_ops.update_settings_ui(self, context)
diff --git a/uv_magic_uv/legacy/properites.py b/uv_magic_uv/legacy/properites.py
new file mode 100644
index 00000000..b64a48f3
--- /dev/null
+++ b/uv_magic_uv/legacy/properites.py
@@ -0,0 +1,61 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+
+from ..utils.property_class_registry import PropertyClassRegistry
+
+__all__ = [
+ 'MUV_Properties',
+ 'init_props',
+ 'clear_props',
+]
+
+
+# Properties used in this add-on.
+# pylint: disable=W0612
+class MUV_Properties():
+ def __init__(self):
+ self.prefs = MUV_Prefs()
+
+
+class MUV_Prefs():
+ expanded = {
+ "info_desc": False,
+ "info_loc": False,
+ "conf_uvsculpt": False,
+ "conf_uvinsp": False,
+ "conf_texproj": False,
+ "conf_uvbb": False
+ }
+
+
+def init_props(scene):
+ scene.muv_props = MUV_Properties()
+ PropertyClassRegistry.init_props(scene)
+
+
+def clear_props(scene):
+ PropertyClassRegistry.del_props(scene)
+ del scene.muv_props
diff --git a/uv_magic_uv/legacy/ui/IMAGE_MT_uvs.py b/uv_magic_uv/legacy/ui/IMAGE_MT_uvs.py
new file mode 100644
index 00000000..bf071bf5
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/IMAGE_MT_uvs.py
@@ -0,0 +1,197 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import (
+ align_uv_cursor,
+ copy_paste_uv_uvedit,
+ align_uv,
+ select_uv,
+ uv_inspection
+)
+from ...utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_MT_CopyPasteUV_UVEdit',
+ 'MUV_MT_AlignUV',
+ 'MUV_MT_SelectUV',
+ 'MUV_MT_AlignUVCursor',
+ 'MUV_MT_UVInspection',
+]
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_CopyPasteUV_UVEdit(bpy.types.Menu):
+ """
+ Menu class: Master menu of Copy/Paste UV coordinate on UV/ImageEditor
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_uvedit_menu"
+ bl_label = "Copy/Paste UV"
+ bl_description = "Copy and Paste UV coordinate among object"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.operator(
+ copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname,
+ text="Copy")
+ layout.operator(
+ copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname,
+ text="Paste")
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_AlignUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of Align UV
+ """
+
+ bl_idname = "uv.muv_align_uv_menu"
+ bl_label = "Align UV"
+ bl_description = "Align UV"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ ops = layout.operator(align_uv.MUV_OT_AlignUV_Circle.bl_idname,
+ text="Circle")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+
+ ops = layout.operator(align_uv.MUV_OT_AlignUV_Straighten.bl_idname,
+ text="Straighten")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+ ops.vertical = sc.muv_align_uv_vertical
+ ops.horizontal = sc.muv_align_uv_horizontal
+
+ ops = layout.operator(align_uv.MUV_OT_AlignUV_Axis.bl_idname,
+ text="XY-axis")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+ ops.vertical = sc.muv_align_uv_vertical
+ ops.horizontal = sc.muv_align_uv_horizontal
+ ops.location = sc.muv_align_uv_location
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_SelectUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of Select UV
+ """
+
+ bl_idname = "uv.muv_select_uv_menu"
+ bl_label = "Select UV"
+ bl_description = "Select UV"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.operator(select_uv.MUV_OT_SelectUV_SelectOverlapped.bl_idname,
+ text="Overlapped")
+ layout.operator(select_uv.MUV_OT_SelectUV_SelectFlipped.bl_idname,
+ text="Flipped")
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_AlignUVCursor(bpy.types.Menu):
+ """
+ Menu class: Master menu of Align UV Cursor
+ """
+
+ bl_idname = "uv.muv_align_uv_cursor_menu"
+ bl_label = "Align UV Cursor"
+ bl_description = "Align UV cursor"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Left Top")
+ ops.position = 'LEFT_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Middle Top")
+ ops.position = 'MIDDLE_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Top")
+ ops.position = 'RIGHT_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Left Middle")
+ ops.position = 'LEFT_MIDDLE'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Center")
+ ops.position = 'CENTER'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Middle")
+ ops.position = 'RIGHT_MIDDLE'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Left Bottom")
+ ops.position = 'LEFT_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Middle Bottom")
+ ops.position = 'MIDDLE_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Bottom")
+ ops.position = 'RIGHT_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_UVInspection(bpy.types.Menu):
+ """
+ Menu class: Master menu of UV Inspection
+ """
+
+ bl_idname = "uv.muv_uv_inspection_menu"
+ bl_label = "UV Inspection"
+ bl_description = "UV Inspection"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.prop(sc, "muv_uv_inspection_show", text="UV Inspection")
+ layout.operator(uv_inspection.MUV_OT_UVInspection_Update.bl_idname,
+ text="Update")
diff --git a/uv_magic_uv/legacy/ui/VIEW3D_MT_object.py b/uv_magic_uv/legacy/ui/VIEW3D_MT_object.py
new file mode 100644
index 00000000..e1c751ae
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/VIEW3D_MT_object.py
@@ -0,0 +1,54 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import copy_paste_uv_object
+from ...utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_MT_CopyPasteUV_Object',
+]
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_CopyPasteUV_Object(bpy.types.Menu):
+ """
+ Menu class: Master menu of Copy/Paste UV coordinate among object
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_object_menu"
+ bl_label = "Copy/Paste UV"
+ bl_description = "Copy and Paste UV coordinate among object"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.menu(
+ copy_paste_uv_object.MUV_MT_CopyPasteUVObject_CopyUV.bl_idname,
+ text="Copy")
+ layout.menu(
+ copy_paste_uv_object.MUV_MT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="Paste")
diff --git a/uv_magic_uv/legacy/ui/VIEW3D_MT_uv_map.py b/uv_magic_uv/legacy/ui/VIEW3D_MT_uv_map.py
new file mode 100644
index 00000000..d229322a
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/VIEW3D_MT_uv_map.py
@@ -0,0 +1,257 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+import bpy.utils
+
+from ..op import (
+ texture_lock,
+ copy_paste_uv,
+ preserve_uv_aspect,
+ texture_projection,
+ texture_wrap,
+ transfer_uv,
+ uvw,
+ world_scale_uv
+)
+from ..op.world_scale_uv import MUV_OT_WorldScaleUV_ApplyProportionalToMesh
+from ...utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_MT_CopyPasteUV',
+ 'MUV_MT_TransferUV',
+ 'MUV_MT_TextureLock',
+ 'MUV_MT_WorldScaleUV',
+ 'MUV_MT_TextureWrap',
+ 'MUV_MT_UVW',
+ 'MUV_MT_TextureProjection',
+ 'MUV_MT_PreserveUVAspect',
+]
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_CopyPasteUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of Copy/Paste UV coordinate
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_menu"
+ bl_label = "Copy/Paste UV"
+ bl_description = "Copy and Paste UV coordinate"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.label(text="Default")
+ layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_CopyUV.bl_idname,
+ text="Copy")
+ layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_PasteUV.bl_idname,
+ text="Paste")
+
+ layout.separator()
+
+ layout.label(text="Selection Sequence")
+ layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="Copy")
+ layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="Paste")
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_TransferUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of Transfer UV coordinate
+ """
+
+ bl_idname = "uv.muv_transfer_uv_menu"
+ bl_label = "Transfer UV"
+ bl_description = "Transfer UV coordinate"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.operator(transfer_uv.MUV_OT_TransferUV_CopyUV.bl_idname,
+ text="Copy")
+ ops = layout.operator(transfer_uv.MUV_OT_TransferUV_PasteUV.bl_idname,
+ text="Paste")
+ ops.invert_normals = sc.muv_transfer_uv_invert_normals
+ ops.copy_seams = sc.muv_transfer_uv_copy_seams
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_TextureLock(bpy.types.Menu):
+ """
+ Menu class: Master menu of Texture Lock
+ """
+
+ bl_idname = "uv.muv_texture_lock_menu"
+ bl_label = "Texture Lock"
+ bl_description = "Lock texture when vertices of mesh (Preserve UV)"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.label("Normal Mode")
+ layout.operator(
+ texture_lock.MUV_OT_TextureLock_Lock.bl_idname,
+ text="Lock"
+ if not texture_lock.MUV_OT_TextureLock_Lock.is_ready(context)
+ else "ReLock")
+ ops = layout.operator(texture_lock.MUV_OT_TextureLock_Unlock.bl_idname,
+ text="Unlock")
+ ops.connect = sc.muv_texture_lock_connect
+
+ layout.separator()
+
+ layout.label("Interactive Mode")
+ layout.prop(sc, "muv_texture_lock_lock", text="Lock")
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_WorldScaleUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of world scale UV
+ """
+
+ bl_idname = "uv.muv_world_scale_uv_menu"
+ bl_label = "World Scale UV"
+ bl_description = ""
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.operator(world_scale_uv.MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ layout.operator(
+ world_scale_uv.MUV_OT_WorldScaleUV_ApplyManual.bl_idname,
+ text="Apply (Manual)")
+
+ ops = layout.operator(
+ world_scale_uv.MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname,
+ text="Apply (Same Desity)")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.same_density = True
+
+ ops = layout.operator(
+ world_scale_uv.MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname,
+ text="Apply (Scaling Desity)")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.same_density = False
+ ops.tgt_scaling_factor = sc.muv_world_scale_uv_tgt_scaling_factor
+
+ ops = layout.operator(
+ MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname,
+ text="Apply (Proportional to Mesh)")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area
+ ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area
+ ops.origin = sc.muv_world_scale_uv_origin
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_TextureWrap(bpy.types.Menu):
+ """
+ Menu class: Master menu of Texture Wrap
+ """
+
+ bl_idname = "uv.muv_texture_wrap_menu"
+ bl_label = "Texture Wrap"
+ bl_description = ""
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.operator(texture_wrap.MUV_OT_TextureWrap_Refer.bl_idname,
+ text="Refer")
+ layout.operator(texture_wrap.MUV_OT_TextureWrap_Set.bl_idname,
+ text="Set")
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_UVW(bpy.types.Menu):
+ """
+ Menu class: Master menu of UVW
+ """
+
+ bl_idname = "uv.muv_uvw_menu"
+ bl_label = "UVW"
+ bl_description = ""
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ ops = layout.operator(uvw.MUV_OT_UVW_BoxMap.bl_idname, text="Box")
+ ops.assign_uvmap = sc.muv_uvw_assign_uvmap
+
+ ops = layout.operator(uvw.MUV_OT_UVW_BestPlanerMap.bl_idname,
+ text="Best Planner")
+ ops.assign_uvmap = sc.muv_uvw_assign_uvmap
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_TextureProjection(bpy.types.Menu):
+ """
+ Menu class: Master menu of Texture Projection
+ """
+
+ bl_idname = "uv.muv_texture_projection_menu"
+ bl_label = "Texture Projection"
+ bl_description = ""
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.prop(sc, "muv_texture_projection_enable",
+ text="Texture Projection")
+ layout.operator(
+ texture_projection.MUV_OT_TextureProjection_Project.bl_idname,
+ text="Project")
+
+
+@BlClassRegistry(legacy=True)
+class MUV_MT_PreserveUVAspect(bpy.types.Menu):
+ """
+ Menu class: Master menu of Preserve UV Aspect
+ """
+
+ bl_idname = "uv.muv_preserve_uv_aspect_menu"
+ bl_label = "Preserve UV Aspect"
+ bl_description = ""
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ for key in bpy.data.images.keys():
+ ops = layout.operator(
+ preserve_uv_aspect.MUV_OT_PreserveUVAspect.bl_idname, text=key)
+ ops.dest_img_name = key
+ ops.origin = sc.muv_preserve_uv_aspect_origin
diff --git a/uv_magic_uv/legacy/ui/__init__.py b/uv_magic_uv/legacy/ui/__init__.py
new file mode 100644
index 00000000..bf790a8e
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/__init__.py
@@ -0,0 +1,50 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(view3d_copy_paste_uv_objectmode)
+ importlib.reload(view3d_copy_paste_uv_editmode)
+ importlib.reload(view3d_uv_manipulation)
+ importlib.reload(view3d_uv_mapping)
+ importlib.reload(uvedit_copy_paste_uv)
+ importlib.reload(uvedit_uv_manipulation)
+ importlib.reload(uvedit_editor_enhancement)
+ importlib.reload(VIEW3D_MT_uv_map)
+ importlib.reload(VIEW3D_MT_object)
+ importlib.reload(IMAGE_MT_uvs)
+else:
+ from . import view3d_copy_paste_uv_objectmode
+ from . import view3d_copy_paste_uv_editmode
+ from . import view3d_uv_manipulation
+ from . import view3d_uv_mapping
+ from . import uvedit_copy_paste_uv
+ from . import uvedit_uv_manipulation
+ from . import uvedit_editor_enhancement
+ from . import VIEW3D_MT_uv_map
+ from . import VIEW3D_MT_object
+ from . import IMAGE_MT_uvs
+
+import bpy \ No newline at end of file
diff --git a/uv_magic_uv/legacy/ui/uvedit_copy_paste_uv.py b/uv_magic_uv/legacy/ui/uvedit_copy_paste_uv.py
new file mode 100644
index 00000000..9848f03b
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/uvedit_copy_paste_uv.py
@@ -0,0 +1,62 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import copy_paste_uv_uvedit
+from ...utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_PT_UVEdit_CopyPasteUV',
+]
+
+
+@BlClassRegistry(legacy=True)
+class MUV_PT_UVEdit_CopyPasteUV(bpy.types.Panel):
+ """
+ Panel class: Copy/Paste UV on Property Panel on UV/ImageEditor
+ """
+
+ bl_space_type = 'IMAGE_EDITOR'
+ bl_region_type = 'TOOLS'
+ bl_label = "Copy/Paste UV"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon='IMAGE_COL')
+
+ def draw(self, _):
+ layout = self.layout
+
+ row = layout.row(align=True)
+ row.operator(
+ copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname,
+ text="Copy")
+ row.operator(
+ copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname,
+ text="Paste")
diff --git a/uv_magic_uv/legacy/ui/uvedit_editor_enhancement.py b/uv_magic_uv/legacy/ui/uvedit_editor_enhancement.py
new file mode 100644
index 00000000..3f750feb
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/uvedit_editor_enhancement.py
@@ -0,0 +1,149 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import (
+ align_uv_cursor,
+ uv_bounding_box,
+ uv_inspection,
+)
+from ...utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_PT_UVEdit_EditorEnhancement',
+]
+
+
+@BlClassRegistry(legacy=True)
+class MUV_PT_UVEdit_EditorEnhancement(bpy.types.Panel):
+ """
+ Panel class: UV/Image Editor Enhancement
+ """
+
+ bl_space_type = 'IMAGE_EDITOR'
+ bl_region_type = 'TOOLS'
+ bl_label = "Editor Enhancement"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon='IMAGE_COL')
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ box = layout.box()
+ box.prop(sc, "muv_align_uv_cursor_enabled", text="Align UV Cursor")
+ if sc.muv_align_uv_cursor_enabled:
+ box.prop(sc, "muv_align_uv_cursor_align_method", expand=True)
+
+ col = box.column(align=True)
+
+ row = col.row(align=True)
+ ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Left Top")
+ ops.position = 'LEFT_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Middle Top")
+ ops.position = 'MIDDLE_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Top")
+ ops.position = 'RIGHT_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ row = col.row(align=True)
+ ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Left Middle")
+ ops.position = 'LEFT_MIDDLE'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Center")
+ ops.position = 'CENTER'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Middle")
+ ops.position = 'RIGHT_MIDDLE'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ row = col.row(align=True)
+ ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Left Bottom")
+ ops.position = 'LEFT_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Middle Bottom")
+ ops.position = 'MIDDLE_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Bottom")
+ ops.position = 'RIGHT_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ box = layout.box()
+ box.prop(sc, "muv_uv_cursor_location_enabled",
+ text="UV Cursor Location")
+ if sc.muv_uv_cursor_location_enabled:
+ box.prop(sc, "muv_align_uv_cursor_cursor_loc", text="")
+
+ box = layout.box()
+ box.prop(sc, "muv_uv_bounding_box_enabled", text="UV Bounding Box")
+ if sc.muv_uv_bounding_box_enabled:
+ box.prop(
+ sc, "muv_uv_bounding_box_show",
+ text="Hide"
+ if uv_bounding_box.MUV_OT_UVBoundingBox.is_running(context)
+ else "Show",
+ icon='RESTRICT_VIEW_OFF'
+ if uv_bounding_box.MUV_OT_UVBoundingBox.is_running(context)
+ else 'RESTRICT_VIEW_ON')
+ box.prop(sc, "muv_uv_bounding_box_uniform_scaling",
+ text="Uniform Scaling")
+ box.prop(sc, "muv_uv_bounding_box_boundary", text="Boundary")
+
+ box = layout.box()
+ box.prop(sc, "muv_uv_inspection_enabled", text="UV Inspection")
+ if sc.muv_uv_inspection_enabled:
+ row = box.row()
+ row.prop(
+ sc, "muv_uv_inspection_show",
+ text="Hide"
+ if uv_inspection.MUV_OT_UVInspection_Render.is_running(context)
+ else "Show",
+ icon='RESTRICT_VIEW_OFF'
+ if uv_inspection.MUV_OT_UVInspection_Render.is_running(context)
+ else 'RESTRICT_VIEW_ON')
+ row.operator(uv_inspection.MUV_OT_UVInspection_Update.bl_idname,
+ text="Update")
+ row = box.row()
+ row.prop(sc, "muv_uv_inspection_show_overlapped")
+ row.prop(sc, "muv_uv_inspection_show_flipped")
+ row = box.row()
+ row.prop(sc, "muv_uv_inspection_show_mode")
diff --git a/uv_magic_uv/legacy/ui/uvedit_uv_manipulation.py b/uv_magic_uv/legacy/ui/uvedit_uv_manipulation.py
new file mode 100644
index 00000000..96cf17d3
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/uvedit_uv_manipulation.py
@@ -0,0 +1,130 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import (
+ align_uv,
+ smooth_uv,
+ pack_uv,
+ select_uv,
+)
+from ...utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_PT_UVEdit_UVManipulation',
+]
+
+
+@BlClassRegistry(legacy=True)
+class MUV_PT_UVEdit_UVManipulation(bpy.types.Panel):
+ """
+ Panel class: UV Manipulation on Property Panel on UV/ImageEditor
+ """
+
+ bl_space_type = 'IMAGE_EDITOR'
+ bl_region_type = 'TOOLS'
+ bl_label = "UV Manipulation"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon='IMAGE_COL')
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+
+ box = layout.box()
+ box.prop(sc, "muv_align_uv_enabled", text="Align UV")
+ if sc.muv_align_uv_enabled:
+ col = box.column()
+ row = col.row(align=True)
+ ops = row.operator(align_uv.MUV_OT_AlignUV_Circle.bl_idname,
+ text="Circle")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+ ops = row.operator(align_uv.MUV_OT_AlignUV_Straighten.bl_idname,
+ text="Straighten")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+ ops.vertical = sc.muv_align_uv_vertical
+ ops.horizontal = sc.muv_align_uv_horizontal
+ ops.mesh_infl = sc.muv_align_uv_mesh_infl
+ row = col.row()
+ ops = row.operator(align_uv.MUV_OT_AlignUV_Axis.bl_idname,
+ text="XY-axis")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+ ops.vertical = sc.muv_align_uv_vertical
+ ops.horizontal = sc.muv_align_uv_horizontal
+ ops.location = sc.muv_align_uv_location
+ ops.mesh_infl = sc.muv_align_uv_mesh_infl
+ row.prop(sc, "muv_align_uv_location", text="")
+
+ col = box.column(align=True)
+ row = col.row(align=True)
+ row.prop(sc, "muv_align_uv_transmission", text="Transmission")
+ row.prop(sc, "muv_align_uv_select", text="Select")
+ row = col.row(align=True)
+ row.prop(sc, "muv_align_uv_vertical", text="Vertical")
+ row.prop(sc, "muv_align_uv_horizontal", text="Horizontal")
+ col.prop(sc, "muv_align_uv_mesh_infl", text="Mesh Influence")
+
+ box = layout.box()
+ box.prop(sc, "muv_smooth_uv_enabled", text="Smooth UV")
+ if sc.muv_smooth_uv_enabled:
+ ops = box.operator(smooth_uv.MUV_OT_SmoothUV.bl_idname,
+ text="Smooth")
+ ops.transmission = sc.muv_smooth_uv_transmission
+ ops.select = sc.muv_smooth_uv_select
+ ops.mesh_infl = sc.muv_smooth_uv_mesh_infl
+ col = box.column(align=True)
+ row = col.row(align=True)
+ row.prop(sc, "muv_smooth_uv_transmission", text="Transmission")
+ row.prop(sc, "muv_smooth_uv_select", text="Select")
+ col.prop(sc, "muv_smooth_uv_mesh_infl", text="Mesh Influence")
+
+ box = layout.box()
+ box.prop(sc, "muv_select_uv_enabled", text="Select UV")
+ if sc.muv_select_uv_enabled:
+ row = box.row(align=True)
+ row.operator(select_uv.MUV_OT_SelectUV_SelectOverlapped.bl_idname)
+ row.operator(select_uv.MUV_OT_SelectUV_SelectFlipped.bl_idname)
+
+ box = layout.box()
+ box.prop(sc, "muv_pack_uv_enabled", text="Pack UV (Extension)")
+ if sc.muv_pack_uv_enabled:
+ ops = box.operator(pack_uv.MUV_OT_PackUV.bl_idname, text="Pack UV")
+ ops.allowable_center_deviation = \
+ sc.muv_pack_uv_allowable_center_deviation
+ ops.allowable_size_deviation = \
+ sc.muv_pack_uv_allowable_size_deviation
+ box.label("Allowable Center Deviation:")
+ box.prop(sc, "muv_pack_uv_allowable_center_deviation", text="")
+ box.label("Allowable Size Deviation:")
+ box.prop(sc, "muv_pack_uv_allowable_size_deviation", text="")
diff --git a/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_editmode.py b/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_editmode.py
new file mode 100644
index 00000000..ee2713b2
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_editmode.py
@@ -0,0 +1,93 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import (
+ transfer_uv,
+ copy_paste_uv,
+)
+from ...utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_PT_View3D_Edit_CopyPasteUV',
+]
+
+
+@BlClassRegistry(legacy=True)
+class MUV_PT_View3D_Edit_CopyPasteUV(bpy.types.Panel):
+ """
+ Panel class: Copy/Paste UV on Property Panel on View3D
+ """
+
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'TOOLS'
+ bl_label = "Copy/Paste UV"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon='IMAGE_COL')
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+
+ box = layout.box()
+ box.prop(sc, "muv_copy_paste_uv_enabled", text="Copy/Paste UV")
+ if sc.muv_copy_paste_uv_enabled:
+ row = box.row(align=True)
+ if sc.muv_copy_paste_uv_mode == 'DEFAULT':
+ row.menu(copy_paste_uv.MUV_MT_CopyPasteUV_CopyUV.bl_idname,
+ text="Copy")
+ row.menu(copy_paste_uv.MUV_MT_CopyPasteUV_PasteUV.bl_idname,
+ text="Paste")
+ elif sc.muv_copy_paste_uv_mode == 'SEL_SEQ':
+ row.menu(
+ copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="Copy")
+ row.menu(
+ copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="Paste")
+ box.prop(sc, "muv_copy_paste_uv_mode", expand=True)
+ box.prop(sc, "muv_copy_paste_uv_copy_seams", text="Seams")
+ box.prop(sc, "muv_copy_paste_uv_strategy", text="Strategy")
+
+ box = layout.box()
+ box.prop(sc, "muv_transfer_uv_enabled", text="Transfer UV")
+ if sc.muv_transfer_uv_enabled:
+ row = box.row(align=True)
+ row.operator(transfer_uv.MUV_OT_TransferUV_CopyUV.bl_idname,
+ text="Copy")
+ ops = row.operator(transfer_uv.MUV_OT_TransferUV_PasteUV.bl_idname,
+ text="Paste")
+ ops.invert_normals = sc.muv_transfer_uv_invert_normals
+ ops.copy_seams = sc.muv_transfer_uv_copy_seams
+ row = box.row()
+ row.prop(sc, "muv_transfer_uv_invert_normals",
+ text="Invert Normals")
+ row.prop(sc, "muv_transfer_uv_copy_seams", text="Seams")
diff --git a/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_objectmode.py b/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_objectmode.py
new file mode 100644
index 00000000..58031b0f
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_objectmode.py
@@ -0,0 +1,65 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import copy_paste_uv_object
+from ...utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_PT_View3D_Object_CopyPasteUV',
+]
+
+
+@BlClassRegistry(legacy=True)
+class MUV_PT_View3D_Object_CopyPasteUV(bpy.types.Panel):
+ """
+ Panel class: Copy/Paste UV on Property Panel on View3D
+ """
+
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'TOOLS'
+ bl_label = "Copy/Paste UV"
+ bl_category = "Magic UV"
+ bl_context = 'objectmode'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon='IMAGE_COL')
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+
+ row = layout.row(align=True)
+ row.menu(
+ copy_paste_uv_object.MUV_MT_CopyPasteUVObject_CopyUV.bl_idname,
+ text="Copy")
+ row.menu(
+ copy_paste_uv_object.MUV_MT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="Paste")
+ layout.prop(sc, "muv_copy_paste_uv_object_copy_seams",
+ text="Seams")
diff --git a/uv_magic_uv/legacy/ui/view3d_uv_manipulation.py b/uv_magic_uv/legacy/ui/view3d_uv_manipulation.py
new file mode 100644
index 00000000..7d828a38
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/view3d_uv_manipulation.py
@@ -0,0 +1,289 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import (
+ move_uv,
+ flip_rotate_uv,
+ mirror_uv,
+ preserve_uv_aspect,
+ texture_lock,
+ texture_wrap,
+ uv_sculpt,
+ world_scale_uv,
+)
+from ..op.world_scale_uv import (
+ MUV_OT_WorldScaleUV_ApplyProportionalToMesh,
+ MUV_OT_WorldScaleUV_ApplyScalingDensity
+)
+from ...utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_PT_View3D_UVManipulation',
+]
+
+
+@BlClassRegistry(legacy=True)
+class MUV_PT_View3D_UVManipulation(bpy.types.Panel):
+ """
+ Panel class: UV Manipulation on Property Panel on View3D
+ """
+
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'TOOLS'
+ bl_label = "UV Manipulation"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon='IMAGE_COL')
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+
+ box = layout.box()
+ box.prop(sc, "muv_flip_rotate_uv_enabled", text="Flip/Rotate UV")
+ if sc.muv_flip_rotate_uv_enabled:
+ row = box.row()
+ ops = row.operator(flip_rotate_uv.MUV_OT_FlipRotate.bl_idname,
+ text="Flip/Rotate")
+ ops.seams = sc.muv_flip_rotate_uv_seams
+ row.prop(sc, "muv_flip_rotate_uv_seams", text="Seams")
+
+ box = layout.box()
+ box.prop(sc, "muv_mirror_uv_enabled", text="Mirror UV")
+ if sc.muv_mirror_uv_enabled:
+ row = box.row()
+ ops = row.operator(mirror_uv.MUV_OT_MirrorUV.bl_idname,
+ text="Mirror")
+ ops.axis = sc.muv_mirror_uv_axis
+ row.prop(sc, "muv_mirror_uv_axis", text="")
+
+ box = layout.box()
+ box.prop(sc, "muv_move_uv_enabled", text="Move UV")
+ if sc.muv_move_uv_enabled:
+ col = box.column()
+ if not move_uv.MUV_OT_MoveUV.is_running(context):
+ col.operator(move_uv.MUV_OT_MoveUV.bl_idname, icon='PLAY',
+ text="Start")
+ else:
+ col.operator(move_uv.MUV_OT_MoveUV.bl_idname, icon='PAUSE',
+ text="Stop")
+
+ box = layout.box()
+ box.prop(sc, "muv_world_scale_uv_enabled", text="World Scale UV")
+ if sc.muv_world_scale_uv_enabled:
+ box.prop(sc, "muv_world_scale_uv_mode", text="")
+
+ if sc.muv_world_scale_uv_mode == 'MANUAL':
+ sp = box.split(percentage=0.5)
+ col = sp.column()
+ col.prop(sc, "muv_world_scale_uv_tgt_texture_size",
+ text="Texture Size")
+ sp = sp.split(percentage=1.0)
+ col = sp.column()
+ col.label("Density:")
+ col.prop(sc, "muv_world_scale_uv_tgt_density", text="")
+ box.prop(sc, "muv_world_scale_uv_origin", text="Origin")
+ ops = box.operator(
+ world_scale_uv.MUV_OT_WorldScaleUV_ApplyManual.bl_idname,
+ text="Apply")
+ ops.tgt_density = sc.muv_world_scale_uv_tgt_density
+ ops.tgt_texture_size = sc.muv_world_scale_uv_tgt_texture_size
+ ops.origin = sc.muv_world_scale_uv_origin
+ ops.show_dialog = False
+
+ elif sc.muv_world_scale_uv_mode == 'SAME_DENSITY':
+ sp = box.split(percentage=0.4)
+ col = sp.column(align=True)
+ col.label("Source:")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.operator(
+ world_scale_uv.MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ sp = box.split(percentage=0.7)
+ col = sp.column(align=True)
+ col.prop(sc, "muv_world_scale_uv_src_density", text="Density")
+ col.enabled = False
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("px2/cm2")
+
+ box.separator()
+ box.prop(sc, "muv_world_scale_uv_origin", text="Origin")
+ ops = box.operator(
+ MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname,
+ text="Apply")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.origin = sc.muv_world_scale_uv_origin
+ ops.same_density = True
+ ops.show_dialog = False
+
+ elif sc.muv_world_scale_uv_mode == 'SCALING_DENSITY':
+ sp = box.split(percentage=0.4)
+ col = sp.column(align=True)
+ col.label("Source:")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.operator(
+ world_scale_uv.MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ sp = box.split(percentage=0.7)
+ col = sp.column(align=True)
+ col.prop(sc, "muv_world_scale_uv_src_density", text="Density")
+ col.enabled = False
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("px2/cm2")
+
+ box.separator()
+ box.prop(sc, "muv_world_scale_uv_tgt_scaling_factor",
+ text="Scaling Factor")
+ box.prop(sc, "muv_world_scale_uv_origin", text="Origin")
+ ops = box.operator(
+ MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname,
+ text="Apply")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.origin = sc.muv_world_scale_uv_origin
+ ops.same_density = False
+ ops.show_dialog = False
+ ops.tgt_scaling_factor = \
+ sc.muv_world_scale_uv_tgt_scaling_factor
+
+ elif sc.muv_world_scale_uv_mode == 'PROPORTIONAL_TO_MESH':
+ sp = box.split(percentage=0.4)
+ col = sp.column(align=True)
+ col.label("Source:")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.operator(
+ world_scale_uv.MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ sp = box.split(percentage=0.7)
+ col = sp.column(align=True)
+ col.prop(sc, "muv_world_scale_uv_src_mesh_area",
+ text="Mesh Area")
+ col.prop(sc, "muv_world_scale_uv_src_uv_area", text="UV Area")
+ col.prop(sc, "muv_world_scale_uv_src_density", text="Density")
+ col.enabled = False
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("cm2")
+ col.label("px2")
+ col.label("px2/cm2")
+ col.enabled = False
+
+ box.separator()
+ box.prop(sc, "muv_world_scale_uv_origin", text="Origin")
+ ops = box.operator(
+ MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname,
+ text="Apply")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area
+ ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area
+ ops.origin = sc.muv_world_scale_uv_origin
+ ops.show_dialog = False
+
+ box = layout.box()
+ box.prop(sc, "muv_preserve_uv_aspect_enabled",
+ text="Preserve UV Aspect")
+ if sc.muv_preserve_uv_aspect_enabled:
+ row = box.row()
+ ops = row.operator(
+ preserve_uv_aspect.MUV_OT_PreserveUVAspect.bl_idname,
+ text="Change Image")
+ ops.dest_img_name = sc.muv_preserve_uv_aspect_tex_image
+ ops.origin = sc.muv_preserve_uv_aspect_origin
+ row.prop(sc, "muv_preserve_uv_aspect_tex_image", text="")
+ box.prop(sc, "muv_preserve_uv_aspect_origin", text="Origin")
+
+ box = layout.box()
+ box.prop(sc, "muv_texture_lock_enabled", text="Texture Lock")
+ if sc.muv_texture_lock_enabled:
+ row = box.row(align=True)
+ col = row.column(align=True)
+ col.label("Normal Mode:")
+ col = row.column(align=True)
+ col.operator(
+ texture_lock.MUV_OT_TextureLock_Lock.bl_idname,
+ text="Lock"
+ if not texture_lock.MUV_OT_TextureLock_Lock.is_ready(context)
+ else "ReLock")
+ ops = col.operator(
+ texture_lock.MUV_OT_TextureLock_Unlock.bl_idname,
+ text="Unlock")
+ ops.connect = sc.muv_texture_lock_connect
+ col.prop(sc, "muv_texture_lock_connect", text="Connect")
+
+ row = box.row(align=True)
+ row.label("Interactive Mode:")
+ box.prop(
+ sc, "muv_texture_lock_lock",
+ text="Unlock"
+ if texture_lock.MUV_OT_TextureLock_Intr.is_running(context)
+ else "Lock",
+ icon='RESTRICT_VIEW_OFF'
+ if texture_lock.MUV_OT_TextureLock_Intr.is_running(context)
+ else 'RESTRICT_VIEW_ON')
+
+ box = layout.box()
+ box.prop(sc, "muv_texture_wrap_enabled", text="Texture Wrap")
+ if sc.muv_texture_wrap_enabled:
+ row = box.row(align=True)
+ row.operator(texture_wrap.MUV_OT_TextureWrap_Refer.bl_idname,
+ text="Refer")
+ row.operator(texture_wrap.MUV_OT_TextureWrap_Set.bl_idname,
+ text="Set")
+ box.prop(sc, "muv_texture_wrap_set_and_refer")
+ box.prop(sc, "muv_texture_wrap_selseq")
+
+ box = layout.box()
+ box.prop(sc, "muv_uv_sculpt_enabled", text="UV Sculpt")
+ if sc.muv_uv_sculpt_enabled:
+ box.prop(
+ sc, "muv_uv_sculpt_enable",
+ text="Disable"if uv_sculpt.MUV_OT_UVSculpt.is_running(context)
+ else "Enable",
+ icon='RESTRICT_VIEW_OFF'
+ if uv_sculpt.MUV_OT_UVSculpt.is_running(context)
+ else 'RESTRICT_VIEW_ON')
+ col = box.column()
+ col.label("Brush:")
+ col.prop(sc, "muv_uv_sculpt_radius")
+ col.prop(sc, "muv_uv_sculpt_strength")
+ box.prop(sc, "muv_uv_sculpt_tools")
+ if sc.muv_uv_sculpt_tools == 'PINCH':
+ box.prop(sc, "muv_uv_sculpt_pinch_invert")
+ elif sc.muv_uv_sculpt_tools == 'RELAX':
+ box.prop(sc, "muv_uv_sculpt_relax_method")
+ box.prop(sc, "muv_uv_sculpt_show_brush")
diff --git a/uv_magic_uv/legacy/ui/view3d_uv_mapping.py b/uv_magic_uv/legacy/ui/view3d_uv_mapping.py
new file mode 100644
index 00000000..3de86d0d
--- /dev/null
+++ b/uv_magic_uv/legacy/ui/view3d_uv_mapping.py
@@ -0,0 +1,116 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import (
+ uvw,
+ texture_projection,
+ unwrap_constraint,
+)
+from ..op.texture_projection import (
+ MUV_OT_TextureProjection
+)
+from ...utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_PT_View3D_UVMapping',
+]
+
+
+@BlClassRegistry(legacy=True)
+class MUV_PT_View3D_UVMapping(bpy.types.Panel):
+ """
+ Panel class: UV Mapping on Property Panel on View3D
+ """
+
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'TOOLS'
+ bl_label = "UV Mapping"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon='IMAGE_COL')
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+
+ box = layout.box()
+ box.prop(sc, "muv_unwrap_constraint_enabled", text="Unwrap Constraint")
+ if sc.muv_unwrap_constraint_enabled:
+ ops = box.operator(
+ unwrap_constraint.MUV_OT_UnwrapConstraint.bl_idname,
+ text="Unwrap")
+ ops.u_const = sc.muv_unwrap_constraint_u_const
+ ops.v_const = sc.muv_unwrap_constraint_v_const
+ row = box.row(align=True)
+ row.prop(sc, "muv_unwrap_constraint_u_const", text="U-Constraint")
+ row.prop(sc, "muv_unwrap_constraint_v_const", text="V-Constraint")
+
+ box = layout.box()
+ box.prop(sc, "muv_texture_projection_enabled",
+ text="Texture Projection")
+ if sc.muv_texture_projection_enabled:
+ row = box.row()
+ row.prop(
+ sc, "muv_texture_projection_enable",
+ text="Disable"
+ if MUV_OT_TextureProjection.is_running(context)
+ else "Enable",
+ icon='RESTRICT_VIEW_OFF'
+ if MUV_OT_TextureProjection.is_running(context)
+ else 'RESTRICT_VIEW_ON')
+ row.prop(sc, "muv_texture_projection_tex_image", text="")
+ box.prop(sc, "muv_texture_projection_tex_transparency",
+ text="Transparency")
+ col = box.column(align=True)
+ row = col.row()
+ row.prop(sc, "muv_texture_projection_adjust_window",
+ text="Adjust Window")
+ if not sc.muv_texture_projection_adjust_window:
+ row.prop(sc, "muv_texture_projection_tex_magnitude",
+ text="Magnitude")
+ col.prop(sc, "muv_texture_projection_apply_tex_aspect",
+ text="Texture Aspect Ratio")
+ col.prop(sc, "muv_texture_projection_assign_uvmap",
+ text="Assign UVMap")
+ box.operator(
+ texture_projection.MUV_OT_TextureProjection_Project.bl_idname,
+ text="Project")
+
+ box = layout.box()
+ box.prop(sc, "muv_uvw_enabled", text="UVW")
+ if sc.muv_uvw_enabled:
+ row = box.row(align=True)
+ ops = row.operator(uvw.MUV_OT_UVW_BoxMap.bl_idname, text="Box")
+ ops.assign_uvmap = sc.muv_uvw_assign_uvmap
+ ops = row.operator(uvw.MUV_OT_UVW_BestPlanerMap.bl_idname,
+ text="Best Planner")
+ ops.assign_uvmap = sc.muv_uvw_assign_uvmap
+ box.prop(sc, "muv_uvw_assign_uvmap", text="Assign UVMap")
diff --git a/uv_magic_uv/op/__init__.py b/uv_magic_uv/op/__init__.py
index 75885ef6..2142c157 100644
--- a/uv_magic_uv/op/__init__.py
+++ b/uv_magic_uv/op/__init__.py
@@ -20,53 +20,27 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
if "bpy" in locals():
import importlib
- importlib.reload(align_uv)
- importlib.reload(align_uv_cursor)
importlib.reload(copy_paste_uv)
importlib.reload(copy_paste_uv_object)
importlib.reload(copy_paste_uv_uvedit)
importlib.reload(flip_rotate_uv)
importlib.reload(mirror_uv)
importlib.reload(move_uv)
- importlib.reload(pack_uv)
- importlib.reload(preserve_uv_aspect)
- importlib.reload(smooth_uv)
- importlib.reload(texture_lock)
- importlib.reload(texture_projection)
- importlib.reload(texture_wrap)
importlib.reload(transfer_uv)
- importlib.reload(unwrap_constraint)
- importlib.reload(uv_bounding_box)
- importlib.reload(uv_inspection)
- importlib.reload(uv_sculpt)
importlib.reload(uvw)
- importlib.reload(world_scale_uv)
else:
- from . import align_uv
- from . import align_uv_cursor
from . import copy_paste_uv
from . import copy_paste_uv_object
from . import copy_paste_uv_uvedit
from . import flip_rotate_uv
from . import mirror_uv
from . import move_uv
- from . import pack_uv
- from . import preserve_uv_aspect
- from . import smooth_uv
- from . import texture_lock
- from . import texture_projection
- from . import texture_wrap
from . import transfer_uv
- from . import unwrap_constraint
- from . import uv_bounding_box
- from . import uv_inspection
- from . import uv_sculpt
from . import uvw
- from . import world_scale_uv
import bpy
diff --git a/uv_magic_uv/op/copy_paste_uv.py b/uv_magic_uv/op/copy_paste_uv.py
index ee89b5e9..23bc8343 100644
--- a/uv_magic_uv/op/copy_paste_uv.py
+++ b/uv_magic_uv/op/copy_paste_uv.py
@@ -18,121 +18,179 @@
#
# ##### END GPL LICENSE BLOCK #####
-__author__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
-import math
-from math import atan2, sin, cos
-import bpy
import bmesh
+import bpy.utils
from bpy.props import (
StringProperty,
BoolProperty,
IntProperty,
EnumProperty,
)
-from mathutils import Vector
+from ..impl import copy_paste_uv_impl as impl
from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_CopyPasteUV_CopyUV',
+ 'MUV_MT_CopyPasteUV_CopyUV',
+ 'MUV_OT_CopyPasteUV_PasteUV',
+ 'MUV_MT_CopyPasteUV_PasteUV',
+ 'MUV_OT_CopyPasteUV_SelSeqCopyUV',
+ 'MUV_MT_CopyPasteUV_SelSeqCopyUV',
+ 'MUV_OT_CopyPasteUV_SelSeqPasteUV',
+ 'MUV_MT_CopyPasteUV_SelSeqPasteUV',
+]
+
+
+@PropertyClassRegistry()
+class Properties:
+ idname = "copy_paste_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ src_info = None
+
+ scene.muv_props.copy_paste_uv = Props()
+ scene.muv_props.copy_paste_uv_selseq = Props()
+
+ scene.muv_copy_paste_uv_enabled = BoolProperty(
+ name="Copy/Paste UV Enabled",
+ description="Copy/Paste UV is enabled",
+ default=False
+ )
+ scene.muv_copy_paste_uv_copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+ scene.muv_copy_paste_uv_mode = EnumProperty(
+ items=[
+ ('DEFAULT', "Default", "Default Mode"),
+ ('SEL_SEQ', "Selection Sequence", "Selection Sequence Mode")
+ ],
+ name="Copy/Paste UV Mode",
+ description="Copy/Paste UV Mode",
+ default='DEFAULT'
+ )
+ scene.muv_copy_paste_uv_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'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.copy_paste_uv
+ del scene.muv_props.copy_paste_uv_selseq
+ del scene.muv_copy_paste_uv_enabled
+ del scene.muv_copy_paste_uv_copy_seams
+ del scene.muv_copy_paste_uv_mode
+ del scene.muv_copy_paste_uv_strategy
-class MUV_CPUVCopyUV(bpy.types.Operator):
+@BlClassRegistry()
+class MUV_OT_CopyPasteUV_CopyUV(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_idname = "uv.muv_copy_paste_uv_operator_copy_uv"
+ bl_label = "Copy UV"
+ bl_description = "Copy UV coordinate"
bl_options = {'REGISTER', 'UNDO'}
- uv_map = StringProperty(options={'HIDDEN'})
+ uv_map: StringProperty(default="__default", options={'HIDDEN'})
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
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))
+ props = context.scene.muv_props.copy_paste_uv
obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
+ bm = common.create_bmesh(obj)
# 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]
+ uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
# get selected face
- props.src_uvs = []
- props.src_pin_uvs = []
- props.src_seams = []
- 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]
- seams = [l.edge.seam for l in face.loops]
- props.src_uvs.append(uvs)
- props.src_pin_uvs.append(pin_uvs)
- props.src_seams.append(seams)
- if not props.src_uvs or not props.src_pin_uvs:
- self.report({'WARNING'}, "No faces are selected")
+ src_info = impl.get_src_face_info(self, bm, uv_layers)
+ if src_info is None:
return {'CANCELLED'}
- self.report({'INFO'}, "%d face(s) are selected" % len(props.src_uvs))
+ props.src_info = src_info
+
+ face_count = len(props.src_info[list(props.src_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are copied".format(face_count))
return {'FINISHED'}
-class MUV_CPUVCopyUVMenu(bpy.types.Menu):
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_CopyUV(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"
+ bl_idname = "uv.muv_copy_paste_uv_menu_copy_uv"
+ bl_label = "Copy UV (Menu)"
+ bl_description = "Menu of Copy UV coordinate"
+
+ @classmethod
+ def poll(cls, context):
+ return impl.is_valid_context(context)
def draw(self, context):
layout = self.layout
# create sub menu
obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
+ bm = common.create_bmesh(obj)
uv_maps = bm.loops.layers.uv.keys()
- layout.operator(
- MUV_CPUVCopyUV.bl_idname,
- text="[Default]",
- icon="IMAGE_COL"
- ).uv_map = ""
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+
for m in uv_maps:
- layout.operator(
- MUV_CPUVCopyUV.bl_idname,
- text=m,
- icon="IMAGE_COL"
- ).uv_map = m
+ ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, text=m)
+ ops.uv_map = m
-class MUV_CPUVPasteUV(bpy.types.Operator):
+@BlClassRegistry()
+class MUV_OT_CopyPasteUV_PasteUV(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_idname = "uv.muv_copy_paste_uv_operator_paste_uv"
+ bl_label = "Paste UV"
+ bl_description = "Paste UV coordinate"
bl_options = {'REGISTER', 'UNDO'}
- uv_map = StringProperty(options={'HIDDEN'})
- strategy = EnumProperty(
+ uv_map: StringProperty(default="__default", options={'HIDDEN'})
+ strategy: EnumProperty(
name="Strategy",
description="Paste Strategy",
items=[
@@ -141,353 +199,209 @@ class MUV_CPUVPasteUV(bpy.types.Operator):
],
default="N_M"
)
- flip_copied_uv = BoolProperty(
+ flip_copied_uv: BoolProperty(
name="Flip Copied UV",
description="Flip Copied UV...",
default=False
)
- rotate_copied_uv = IntProperty(
+ rotate_copied_uv: IntProperty(
default=0,
name="Rotate Copied UV",
min=0,
max=30
)
- copy_seams = BoolProperty(
- name="Copy Seams",
+ copy_seams: BoolProperty(
+ name="Seams",
description="Copy Seams",
default=True
)
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv
+ if not props.src_info:
+ return False
+ return impl.is_valid_context(context)
+
def execute(self, context):
- props = context.scene.muv_props.cpuv
- if not props.src_uvs or not props.src_pin_uvs:
+ props = context.scene.muv_props.copy_paste_uv
+ if not props.src_info:
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 common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
+ bm = common.create_bmesh(obj)
# 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]
+ uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info,
+ self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
# get selected face
- dest_uvs = []
- dest_pin_uvs = []
- dest_seams = []
- 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]
- seams = [l.edge.seam for l in face.loops]
- dest_uvs.append(uvs)
- dest_pin_uvs.append(pin_uvs)
- dest_seams.append(seams)
- if not dest_uvs or not dest_pin_uvs:
- 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)))
+ dest_info = impl.get_dest_face_info(self, bm, uv_layers,
+ props.src_info, self.strategy)
+ if dest_info is None:
return {'CANCELLED'}
# paste
- for i, idx in enumerate(dest_face_indices):
- suv = None
- spuv = None
- ss = None
- duv = None
- if self.strategy == 'N_N':
- suv = props.src_uvs[i]
- spuv = props.src_pin_uvs[i]
- ss = props.src_seams[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)]
- ss = props.src_seams[i % len(props.src_seams)]
- 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]
- ss_fr = [s for s in ss]
- # flip UVs
- if self.flip_copied_uv is True:
- suvs_fr.reverse()
- spuvs_fr.reverse()
- ss_fr.reverse()
- # rotate UVs
- for _ in range(self.rotate_copied_uv):
- uv = suvs_fr.pop()
- pin_uv = spuvs_fr.pop()
- s = ss_fr.pop()
- suvs_fr.insert(0, uv)
- spuvs_fr.insert(0, pin_uv)
- ss_fr.insert(0, s)
- # paste UVs
- for l, suv, spuv, ss in zip(bm.faces[idx].loops, suvs_fr,
- spuvs_fr, ss_fr):
- l[uv_layer].uv = suv
- l[uv_layer].pin_uv = spuv
- if self.copy_seams is True:
- l.edge.seam = ss
- self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs))
+ ret = impl.paste_uv(self, bm, props.src_info, dest_info, uv_layers,
+ self.strategy, self.flip_copied_uv,
+ self.rotate_copied_uv, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
+
+ face_count = len(props.src_info[list(dest_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are pasted".format(face_count))
bmesh.update_edit_mesh(obj.data)
- if self.copy_seams is True:
- obj.data.show_edge_seams = True
return {'FINISHED'}
-class MUV_CPUVPasteUVMenu(bpy.types.Menu):
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_PasteUV(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"
+ bl_idname = "uv.muv_copy_paste_uv_menu_paste_uv"
+ bl_label = "Paste UV (Menu)"
+ bl_description = "Menu of Paste UV coordinate"
+
+ @classmethod
+ def poll(cls, context):
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv
+ if not props.src_info:
+ return False
+ return impl.is_valid_context(context)
def draw(self, context):
sc = context.scene
layout = self.layout
# create sub menu
obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
+ bm = common.create_bmesh(obj)
uv_maps = bm.loops.layers.uv.keys()
- ops = layout.operator(MUV_CPUVPasteUV.bl_idname, text="[Default]")
- ops.uv_map = ""
- ops.copy_seams = sc.muv_cpuv_copy_seams
- ops.strategy = sc.muv_cpuv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname,
+ text="[New]")
+ ops.uv_map = "__new"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
for m in uv_maps:
- ops = layout.operator(MUV_CPUVPasteUV.bl_idname, text=m)
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, text=m)
ops.uv_map = m
- ops.copy_seams = sc.muv_cpuv_copy_seams
- ops.strategy = sc.muv_cpuv_strategy
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
-class MUV_CPUVIECopyUV(bpy.types.Operator):
+@BlClassRegistry()
+class MUV_OT_CopyPasteUV_SelSeqCopyUV(bpy.types.Operator):
"""
- Operation class: Copy UV coordinate on UV/Image Editor
+ Operation class: Copy UV coordinate by selection sequence
"""
- bl_idname = "uv.muv_cpuv_ie_copy_uv"
- bl_label = "Copy UV"
- bl_description = "Copy UV coordinate (only selected in UV/Image Editor)"
+ bl_idname = "uv.muv_copy_paste_uv_operator_selseq_copy_uv"
+ bl_label = "Copy UV (Selection Sequence)"
+ bl_description = "Copy UV data by selection sequence"
bl_options = {'REGISTER', 'UNDO'}
- @classmethod
- def poll(cls, context):
- return context.mode == 'EDIT_MESH'
-
- def execute(self, context):
- props = context.scene.muv_props.cpuv
- obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- uv_layer = bm.loops.layers.uv.verify()
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
-
- for face in bm.faces:
- if not face.select:
- continue
- skip = False
- for l in face.loops:
- if not l[uv_layer].select:
- skip = True
- break
- if skip:
- continue
- props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops])
-
- return {'FINISHED'}
-
-
-class MUV_CPUVIEPasteUV(bpy.types.Operator):
- """
- Operation class: Paste UV coordinate on UV/Image Editor
- """
-
- bl_idname = "uv.muv_cpuv_ie_paste_uv"
- bl_label = "Paste UV"
- bl_description = "Paste UV coordinate (only selected in UV/Image Editor)"
- bl_options = {'REGISTER', 'UNDO'}
+ uv_map: StringProperty(default="__default", options={'HIDDEN'})
@classmethod
def poll(cls, context):
- return context.mode == 'EDIT_MESH'
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
def execute(self, context):
- props = context.scene.muv_props.cpuv
+ props = context.scene.muv_props.copy_paste_uv_selseq
obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- uv_layer = bm.loops.layers.uv.verify()
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
-
- dest_uvs = []
- dest_face_indices = []
- for face in bm.faces:
- if not face.select:
- continue
- skip = False
- for l in face.loops:
- if not l[uv_layer].select:
- skip = True
- break
- if skip:
- continue
- dest_face_indices.append(face.index)
- uvs = [l[uv_layer].uv.copy() for l in face.loops]
- dest_uvs.append(uvs)
-
- for suvs, duvs in zip(props.src_uvs, dest_uvs):
- src_diff = suvs[1] - suvs[0]
- dest_diff = duvs[1] - duvs[0]
-
- src_base = suvs[0]
- dest_base = duvs[0]
-
- src_rad = atan2(src_diff.y, src_diff.x)
- dest_rad = atan2(dest_diff.y, dest_diff.x)
- if src_rad < dest_rad:
- radian = dest_rad - src_rad
- elif src_rad > dest_rad:
- radian = math.pi * 2 - (src_rad - dest_rad)
- else: # src_rad == dest_rad
- radian = 0.0
-
- ratio = dest_diff.length / src_diff.length
- break
-
- for suvs, fidx in zip(props.src_uvs, dest_face_indices):
- for l, suv in zip(bm.faces[fidx].loops, suvs):
- base = suv - src_base
- radian_ref = atan2(base.y, base.x)
- radian_fin = (radian + radian_ref)
- length = base.length
- turn = Vector((length * cos(radian_fin),
- length * sin(radian_fin)))
- target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \
- dest_base
- l[uv_layer].uv = target_uv
-
- bmesh.update_edit_mesh(obj.data)
-
- return {'FINISHED'}
-
-
-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 common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
+ bm = common.create_bmesh(obj)
# 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]
+ uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
# get selected face
- props.src_uvs = []
- props.src_pin_uvs = []
- props.src_seams = []
- 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]
- seams = [l.edge.seam for l in hist.loops]
- props.src_uvs.append(uvs)
- props.src_pin_uvs.append(pin_uvs)
- props.src_seams.append(seams)
- if not props.src_uvs or not props.src_pin_uvs:
- self.report({'WARNING'}, "No faces are selected")
+ src_info = impl.get_select_history_src_face_info(self, bm, uv_layers)
+ if src_info is None:
return {'CANCELLED'}
- self.report({'INFO'}, "%d face(s) are selected" % len(props.src_uvs))
+ props.src_info = src_info
+
+ face_count = len(props.src_info[list(props.src_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are selected".format(face_count))
return {'FINISHED'}
-class MUV_CPUVSelSeqCopyUVMenu(bpy.types.Menu):
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_SelSeqCopyUV(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"
+ bl_idname = "uv.muv_copy_paste_uv_menu_selseq_copy_uv"
+ bl_label = "Copy UV (Selection Sequence) (Menu)"
+ bl_description = "Menu of Copy UV coordinate by selection sequence"
+
+ @classmethod
+ def poll(cls, context):
+ return impl.is_valid_context(context)
def draw(self, context):
layout = self.layout
obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
+ bm = common.create_bmesh(obj)
uv_maps = bm.loops.layers.uv.keys()
- layout.operator(
- MUV_CPUVSelSeqCopyUV.bl_idname,
- text="[Default]", icon="IMAGE_COL").uv_map = ""
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+
for m in uv_maps:
- layout.operator(
- MUV_CPUVSelSeqCopyUV.bl_idname,
- text=m, icon="IMAGE_COL").uv_map = m
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text=m)
+ ops.uv_map = m
-class MUV_CPUVSelSeqPasteUV(bpy.types.Operator):
+@BlClassRegistry()
+class MUV_OT_CopyPasteUV_SelSeqPasteUV(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_idname = "uv.muv_copy_paste_uv_operator_selseq_paste_uv"
+ bl_label = "Paste UV (Selection Sequence)"
+ bl_description = "Paste UV coordinate by selection sequence"
bl_options = {'REGISTER', 'UNDO'}
- uv_map = StringProperty(options={'HIDDEN'})
- strategy = EnumProperty(
+ uv_map: StringProperty(default="__default", options={'HIDDEN'})
+ strategy: EnumProperty(
name="Strategy",
description="Paste Strategy",
items=[
@@ -496,151 +410,117 @@ class MUV_CPUVSelSeqPasteUV(bpy.types.Operator):
],
default="N_M"
)
- flip_copied_uv = BoolProperty(
+ flip_copied_uv: BoolProperty(
name="Flip Copied UV",
description="Flip Copied UV...",
default=False
)
- rotate_copied_uv = IntProperty(
+ rotate_copied_uv: IntProperty(
default=0,
name="Rotate Copied UV",
min=0,
max=30
)
- copy_seams = BoolProperty(
- name="Copy Seams",
+ copy_seams: BoolProperty(
+ name="Seams",
description="Copy Seams",
default=True
)
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_selseq
+ if not props.src_info:
+ return False
+ return impl.is_valid_context(context)
+
def execute(self, context):
- props = context.scene.muv_props.cpuv_selseq
- if not props.src_uvs or not props.src_pin_uvs:
+ props = context.scene.muv_props.copy_paste_uv_selseq
+ if not props.src_info:
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 common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
+ bm = common.create_bmesh(obj)
# 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]
+ uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info,
+ self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
# get selected face
- dest_uvs = []
- dest_pin_uvs = []
- dest_seams = []
- 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]
- seams = [l.edge.seam for l in hist.loops]
- dest_uvs.append(uvs)
- dest_pin_uvs.append(pin_uvs)
- dest_seams.append(seams)
- if not dest_uvs or not dest_pin_uvs:
- 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)))
+ dest_info = impl.get_select_history_dest_face_info(self, bm, uv_layers,
+ props.src_info,
+ self.strategy)
+ if dest_info is None:
return {'CANCELLED'}
# paste
- for i, idx in enumerate(dest_face_indices):
- suv = None
- spuv = None
- ss = None
- duv = None
- if self.strategy == 'N_N':
- suv = props.src_uvs[i]
- spuv = props.src_pin_uvs[i]
- ss = props.src_seams[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)]
- ss = props.src_seams[i % len(props.src_seams)]
- 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]
- ss_fr = [s for s in ss]
- # flip UVs
- if self.flip_copied_uv is True:
- suvs_fr.reverse()
- spuvs_fr.reverse()
- ss_fr.reverse()
- # rotate UVs
- for _ in range(self.rotate_copied_uv):
- uv = suvs_fr.pop()
- pin_uv = spuvs_fr.pop()
- s = ss_fr.pop()
- suvs_fr.insert(0, uv)
- spuvs_fr.insert(0, pin_uv)
- ss_fr.insert(0, s)
- # paste UVs
- for l, suv, spuv, ss in zip(bm.faces[idx].loops, suvs_fr,
- spuvs_fr, ss_fr):
- l[uv_layer].uv = suv
- l[uv_layer].pin_uv = spuv
- if self.copy_seams is True:
- l.edge.seam = ss
-
- self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs))
+ ret = impl.paste_uv(self, bm, props.src_info, dest_info, uv_layers,
+ self.strategy, self.flip_copied_uv,
+ self.rotate_copied_uv, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
+
+ face_count = len(props.src_info[list(dest_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are pasted".format(face_count))
bmesh.update_edit_mesh(obj.data)
- if self.copy_seams is True:
- obj.data.show_edge_seams = True
return {'FINISHED'}
-class MUV_CPUVSelSeqPasteUVMenu(bpy.types.Menu):
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_SelSeqPasteUV(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"
+ bl_idname = "uv.muv_copy_paste_uv_menu_selseq_paste_uv"
+ bl_label = "Paste UV (Selection Sequence) (Menu)"
+ bl_description = "Menu of Paste UV coordinate by selection sequence"
+
+ @classmethod
+ def poll(cls, context):
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_selseq
+ if not props.src_uvs or not props.src_pin_uvs:
+ return False
+ return impl.is_valid_context(context)
def draw(self, context):
sc = context.scene
layout = self.layout
# create sub menu
obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
+ bm = common.create_bmesh(obj)
uv_maps = bm.loops.layers.uv.keys()
- ops = layout.operator(MUV_CPUVSelSeqPasteUV.bl_idname,
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
text="[Default]")
- ops.uv_map = ""
- ops.copy_seams = sc.muv_cpuv_copy_seams
- ops.strategy = sc.muv_cpuv_strategy
+ ops.uv_map = "__default"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="[New]")
+ ops.uv_map = "__new"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
for m in uv_maps:
- ops = layout.operator(MUV_CPUVSelSeqPasteUV.bl_idname, text=m)
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text=m)
ops.uv_map = m
- ops.copy_seams = sc.muv_cpuv_copy_seams
- ops.strategy = sc.muv_cpuv_strategy
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
diff --git a/uv_magic_uv/op/copy_paste_uv_object.py b/uv_magic_uv/op/copy_paste_uv_object.py
index d80ee415..d9f42447 100644
--- a/uv_magic_uv/op/copy_paste_uv_object.py
+++ b/uv_magic_uv/op/copy_paste_uv_object.py
@@ -20,17 +20,72 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
-import bpy
import bmesh
+import bpy
from bpy.props import (
StringProperty,
BoolProperty,
)
+from ..impl import copy_paste_uv_impl as impl
from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_CopyPasteUVObject_CopyUV',
+ 'MUV_MT_CopyPasteUVObject_CopyUV',
+ 'MUV_OT_CopyPasteUVObject_PasteUV',
+ 'MUV_MT_CopyPasteUVObject_PasteUV',
+]
+
+
+def is_valid_context(context):
+ obj = context.object
+
+ # only object mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'OBJECT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class Properties:
+ idname = "copy_paste_uv_object"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ src_info = None
+
+ scene.muv_props.copy_paste_uv_object = Props()
+
+ scene.muv_copy_paste_uv_object_copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.copy_paste_uv_object
+ del scene.muv_copy_paste_uv_object_copy_seams
def memorize_view_3d_mode(fn):
@@ -42,197 +97,173 @@ def memorize_view_3d_mode(fn):
return __memorize_view_3d_mode
-class MUV_CPUVObjCopyUV(bpy.types.Operator):
+@BlClassRegistry()
+class MUV_OT_CopyPasteUVObject_CopyUV(bpy.types.Operator):
"""
- Operation class: Copy UV coordinate per object
+ Operation class: Copy UV coordinate among objects
"""
- bl_idname = "object.muv_cpuv_obj_copy_uv"
- bl_label = "Copy UV"
- bl_description = "Copy UV coordinate"
+ bl_idname = "object.muv_copy_paste_uv_object_operator_copy_uv"
+ bl_label = "Copy UV (Among Objects)"
+ bl_description = "Copy UV coordinate (Among Objects)"
bl_options = {'REGISTER', 'UNDO'}
- uv_map = StringProperty(options={'HIDDEN'})
+ uv_map: StringProperty(default="__default", options={'HIDDEN'})
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return is_valid_context(context)
@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))
+ props = context.scene.muv_props.copy_paste_uv_object
bpy.ops.object.mode_set(mode='EDIT')
-
obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
+ bm = common.create_bmesh(obj)
# 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]
+ uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
# get selected face
- props.src_uvs = []
- props.src_pin_uvs = []
- props.src_seams = []
- 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]
- seams = [l.edge.seam for l in face.loops]
- props.src_uvs.append(uvs)
- props.src_pin_uvs.append(pin_uvs)
- props.src_seams.append(seams)
-
- self.report({'INFO'}, "%s's UV coordinates are copied" % (obj.name))
+ src_info = impl.get_src_face_info(self, bm, uv_layers)
+ if src_info is None:
+ return {'CANCELLED'}
+ props.src_info = src_info
+
+ self.report({'INFO'},
+ "{}'s UV coordinates are copied".format(obj.name))
return {'FINISHED'}
-class MUV_CPUVObjCopyUVMenu(bpy.types.Menu):
+@BlClassRegistry()
+class MUV_MT_CopyPasteUVObject_CopyUV(bpy.types.Menu):
"""
- Menu class: Copy UV coordinate per object
+ Menu class: Copy UV coordinate among objects
"""
- bl_idname = "object.muv_cpuv_obj_copy_uv_menu"
- bl_label = "Copy UV"
- bl_description = "Copy UV coordinate per object"
+ bl_idname = "object.muv_copy_paste_uv_object_menu_copy_uv"
+ bl_label = "Copy UV (Among Objects) (Menu)"
+ bl_description = "Menu of Copy UV coordinate (Among Objects)"
+
+ @classmethod
+ def poll(cls, context):
+ return is_valid_context(context)
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]")\
- .uv_map = ""
+ uv_maps = bpy.context.active_object.data.uv_layers.keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+
for m in uv_maps:
- layout.operator(MUV_CPUVObjCopyUV.bl_idname, text=m).uv_map = m
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname,
+ text=m)
+ ops.uv_map = m
-class MUV_CPUVObjPasteUV(bpy.types.Operator):
+@BlClassRegistry()
+class MUV_OT_CopyPasteUVObject_PasteUV(bpy.types.Operator):
"""
- Operation class: Paste UV coordinate per object
+ Operation class: Paste UV coordinate among objects
"""
- bl_idname = "object.muv_cpuv_obj_paste_uv"
- bl_label = "Paste UV"
- bl_description = "Paste UV coordinate"
+ bl_idname = "object.muv_copy_paste_uv_object_operator_paste_uv"
+ bl_label = "Paste UV (Among Objects)"
+ bl_description = "Paste UV coordinate (Among Objects)"
bl_options = {'REGISTER', 'UNDO'}
- uv_map = StringProperty(options={'HIDDEN'})
- copy_seams = BoolProperty(
- name="Copy Seams",
+ uv_map: StringProperty(default="__default", options={'HIDDEN'})
+ copy_seams: BoolProperty(
+ name="Seams",
description="Copy Seams",
default=True
)
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_object
+ if not props.src_info:
+ return False
+ return is_valid_context(context)
+
@memorize_view_3d_mode
def execute(self, context):
- props = context.scene.muv_props.cpuv_obj
- if not props.src_uvs or not props.src_pin_uvs:
+ props = context.scene.muv_props.copy_paste_uv_object
+ if not props.src_info:
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:
+ if not hasattr(o.data, "uv_layers") or not o.select_get():
continue
bpy.ops.object.mode_set(mode='OBJECT')
- bpy.context.scene.objects.active = o
+ bpy.context.view_layer.objects.active = o
bpy.ops.object.mode_set(mode='EDIT')
obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- if 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))
+ bm = common.create_bmesh(obj)
# 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]
+ uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info,
+ self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
# get selected face
- dest_uvs = []
- dest_pin_uvs = []
- dest_seams = []
- 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]
- seams = [l.edge.seam for l in face.loops]
- dest_uvs.append(uvs)
- dest_pin_uvs.append(pin_uvs)
- dest_seams.append(seams)
- 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))
- )
+ dest_info = impl.get_dest_face_info(self, bm, uv_layers,
+ props.src_info, 'N_N')
+ if dest_info is None:
return {'CANCELLED'}
# paste
- for i, idx in enumerate(dest_face_indices):
- suv = props.src_uvs[i]
- spuv = props.src_pin_uvs[i]
- ss = props.src_seams[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]
- ss_fr = [s for s in ss]
- # paste UVs
- for l, suv, spuv, ss in zip(
- bm.faces[idx].loops, suvs_fr, spuvs_fr, ss_fr):
- l[uv_layer].uv = suv
- l[uv_layer].pin_uv = spuv
- if self.copy_seams is True:
- l.edge.seam = ss
+ ret = impl.paste_uv(self, bm, props.src_info, dest_info, uv_layers,
+ 'N_N', 0, 0, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
bmesh.update_edit_mesh(obj.data)
- if self.copy_seams is True:
- obj.data.show_edge_seams = True
self.report(
- {'INFO'}, "%s's UV coordinates are pasted" % (obj.name))
+ {'INFO'}, "{}'s UV coordinates are pasted".format(obj.name))
return {'FINISHED'}
-class MUV_CPUVObjPasteUVMenu(bpy.types.Menu):
+@BlClassRegistry()
+class MUV_MT_CopyPasteUVObject_PasteUV(bpy.types.Menu):
"""
- Menu class: Paste UV coordinate per object
+ Menu class: Paste UV coordinate among objects
"""
- bl_idname = "object.muv_cpuv_obj_paste_uv_menu"
- bl_label = "Paste UV"
- bl_description = "Paste UV coordinate per object"
+ bl_idname = "object.muv_copy_paste_uv_object_menu_paste_uv"
+ bl_label = "Paste UV (Among Objects) (Menu)"
+ bl_description = "Menu of Paste UV coordinate (Among Objects)"
+
+ @classmethod
+ def poll(cls, context):
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_object
+ if not props.src_info:
+ return False
+ return is_valid_context(context)
def draw(self, context):
sc = context.scene
@@ -240,13 +271,26 @@ class MUV_CPUVObjPasteUVMenu(bpy.types.Menu):
# 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))
- ops = layout.operator(MUV_CPUVObjPasteUV.bl_idname, text="[Default]")
- ops.uv_map = ""
- ops.copy_seams = sc.muv_cpuv_copy_seams
+ if hasattr(obj.data, "uv_layers") and obj.select_get():
+ uv_maps.extend(obj.data.uv_layers.keys())
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="[New]")
+ ops.uv_map = "__new"
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
+
for m in uv_maps:
- ops = layout.operator(MUV_CPUVObjPasteUV.bl_idname, text=m)
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text=m)
ops.uv_map = m
- ops.copy_seams = sc.muv_cpuv_copy_seams
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
diff --git a/uv_magic_uv/op/copy_paste_uv_uvedit.py b/uv_magic_uv/op/copy_paste_uv_uvedit.py
index 96908020..719687a6 100644
--- a/uv_magic_uv/op/copy_paste_uv_uvedit.py
+++ b/uv_magic_uv/op/copy_paste_uv_uvedit.py
@@ -18,127 +18,80 @@
#
# ##### END GPL LICENSE BLOCK #####
-__author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester"
+__author__ = "imdjs, Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
-
-import math
-from math import atan2, sin, cos
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
-import bmesh
-from mathutils import Vector
-from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import copy_paste_uv_uvedit_impl as impl
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_CopyPasteUVUVEdit_CopyUV',
+ 'MUV_OT_CopyPasteUVUVEdit_PasteUV',
+]
+
+
+@PropertyClassRegistry()
+class Properties:
+ idname = "copy_paste_uv_uvedit"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ src_uvs = None
+
+ scene.muv_props.copy_paste_uv_uvedit = Props()
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.copy_paste_uv_uvedit
-class MUV_CPUVIECopyUV(bpy.types.Operator):
+@BlClassRegistry()
+class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy.types.Operator):
"""
Operation class: Copy UV coordinate on UV/Image Editor
"""
- bl_idname = "uv.muv_cpuv_ie_copy_uv"
- bl_label = "Copy UV"
+ bl_idname = "uv.muv_copy_paste_uv_uvedit_operator_copy_uv"
+ bl_label = "Copy UV (UV/Image Editor)"
bl_description = "Copy UV coordinate (only selected in UV/Image Editor)"
bl_options = {'REGISTER', 'UNDO'}
+ def __init__(self):
+ self.__impl = impl.CopyUVImpl()
+
@classmethod
def poll(cls, context):
- return context.mode == 'EDIT_MESH'
+ return impl.CopyUVImpl.poll(context)
def execute(self, context):
- props = context.scene.muv_props.cpuv
- obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- uv_layer = bm.loops.layers.uv.verify()
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
-
- for face in bm.faces:
- if not face.select:
- continue
- skip = False
- for l in face.loops:
- if not l[uv_layer].select:
- skip = True
- break
- if skip:
- continue
- props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops])
-
- return {'FINISHED'}
-
-
-class MUV_CPUVIEPasteUV(bpy.types.Operator):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry()
+class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator):
"""
Operation class: Paste UV coordinate on UV/Image Editor
"""
- bl_idname = "uv.muv_cpuv_ie_paste_uv"
- bl_label = "Paste UV"
+ bl_idname = "uv.muv_copy_paste_uv_uvedit_operator_paste_uv"
+ bl_label = "Paste UV (UV/Image Editor)"
bl_description = "Paste UV coordinate (only selected in UV/Image Editor)"
bl_options = {'REGISTER', 'UNDO'}
+ def __init__(self):
+ self.__impl = impl.PasteUVImpl()
+
@classmethod
def poll(cls, context):
- return context.mode == 'EDIT_MESH'
+ return impl.PasteUVImpl.poll(context)
def execute(self, context):
- props = context.scene.muv_props.cpuv
- obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- uv_layer = bm.loops.layers.uv.verify()
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
-
- dest_uvs = []
- dest_face_indices = []
- for face in bm.faces:
- if not face.select:
- continue
- skip = False
- for l in face.loops:
- if not l[uv_layer].select:
- skip = True
- break
- if skip:
- continue
- dest_face_indices.append(face.index)
- uvs = [l[uv_layer].uv.copy() for l in face.loops]
- dest_uvs.append(uvs)
-
- for suvs, duvs in zip(props.src_uvs, dest_uvs):
- src_diff = suvs[1] - suvs[0]
- dest_diff = duvs[1] - duvs[0]
-
- src_base = suvs[0]
- dest_base = duvs[0]
-
- src_rad = atan2(src_diff.y, src_diff.x)
- dest_rad = atan2(dest_diff.y, dest_diff.x)
- if src_rad < dest_rad:
- radian = dest_rad - src_rad
- elif src_rad > dest_rad:
- radian = math.pi * 2 - (src_rad - dest_rad)
- else: # src_rad == dest_rad
- radian = 0.0
-
- ratio = dest_diff.length / src_diff.length
- break
-
- for suvs, fidx in zip(props.src_uvs, dest_face_indices):
- for l, suv in zip(bm.faces[fidx].loops, suvs):
- base = suv - src_base
- radian_ref = atan2(base.y, base.x)
- radian_fin = (radian + radian_ref)
- length = base.length
- turn = Vector((length * cos(radian_fin),
- length * sin(radian_fin)))
- target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \
- dest_base
- l[uv_layer].uv = target_uv
-
- bmesh.update_edit_mesh(obj.data)
-
- return {'FINISHED'}
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/flip_rotate_uv.py b/uv_magic_uv/op/flip_rotate_uv.py
index 30f6b0f7..d1637052 100644
--- a/uv_magic_uv/op/flip_rotate_uv.py
+++ b/uv_magic_uv/op/flip_rotate_uv.py
@@ -20,8 +20,8 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
import bmesh
@@ -31,35 +31,74 @@ from bpy.props import (
)
from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import flip_rotate_impl as impl
+__all__ = [
+ 'Properties',
+ 'MUV_OT_FlipRotate',
+]
-class MUV_FlipRot(bpy.types.Operator):
+
+@PropertyClassRegistry()
+class Properties:
+ idname = "flip_rotate_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_flip_rotate_uv_enabled = BoolProperty(
+ name="Flip/Rotate UV Enabled",
+ description="Flip/Rotate UV is enabled",
+ default=False
+ )
+ scene.muv_flip_rotate_uv_seams = BoolProperty(
+ name="Seams",
+ description="Seams",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_flip_rotate_uv_enabled
+ del scene.muv_flip_rotate_uv_seams
+
+
+@BlClassRegistry()
+class MUV_OT_FlipRotate(bpy.types.Operator):
"""
Operation class: Flip and Rotate UV coordinate
"""
- bl_idname = "uv.muv_fliprot"
+ bl_idname = "uv.muv_flip_rotate_uv_operator"
bl_label = "Flip/Rotate UV"
bl_description = "Flip/Rotate UV coordinate"
bl_options = {'REGISTER', 'UNDO'}
- flip = BoolProperty(
+ flip: BoolProperty(
name="Flip UV",
description="Flip UV...",
default=False
)
- rotate = IntProperty(
+ rotate: IntProperty(
default=0,
name="Rotate UV",
min=0,
max=30
)
- seams = BoolProperty(
+ seams: BoolProperty(
name="Seams",
description="Seams",
default=True
)
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
+
def execute(self, context):
self.report({'INFO'}, "Flip/Rotate UV")
obj = context.active_object
@@ -68,61 +107,24 @@ class MUV_FlipRot(bpy.types.Operator):
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")
+ uv_layer = impl.get_uv_layer(self, bm)
+ if not uv_layer:
return {'CANCELLED'}
- uv_layer = bm.loops.layers.uv.verify()
# get selected face
- dest_uvs = []
- dest_pin_uvs = []
- dest_seams = []
- 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]
- seams = [l.edge.seam for l in face.loops]
- dest_uvs.append(uvs)
- dest_pin_uvs.append(pin_uvs)
- dest_seams.append(seams)
- if not dest_uvs or not dest_pin_uvs:
- self.report({'WARNING'}, "No faces are selected")
+ src_info = impl.get_src_face_info(self, bm, [uv_layer], True)
+ if not src_info:
return {'CANCELLED'}
- self.report({'INFO'}, "%d face(s) are selected" % len(dest_uvs))
+
+ face_count = len(src_info[list(src_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are selected".format(face_count))
# paste
- for idx, duvs, dpuvs, dss in zip(dest_face_indices, dest_uvs,
- dest_pin_uvs, dest_seams):
- duvs_fr = [uv for uv in duvs]
- dpuvs_fr = [pin_uv for pin_uv in dpuvs]
- dss_fr = [s for s in dss]
- # flip UVs
- if self.flip is True:
- duvs_fr.reverse()
- dpuvs_fr.reverse()
- dss_fr.reverse()
- # rotate UVs
- for _ in range(self.rotate):
- uv = duvs_fr.pop()
- pin_uv = dpuvs_fr.pop()
- s = dss_fr.pop()
- duvs_fr.insert(0, uv)
- dpuvs_fr.insert(0, pin_uv)
- dss_fr.insert(0, s)
- # paste UVs
- for l, duv, dpuv, ds in zip(
- bm.faces[idx].loops, duvs_fr, dpuvs_fr, dss_fr):
- l[uv_layer].uv = duv
- l[uv_layer].pin_uv = dpuv
- if self.seams is True:
- l.edge.seam = ds
-
- self.report({'INFO'}, "%d face(s) are flipped/rotated" % len(dest_uvs))
+ ret = impl.paste_uv(self, bm, src_info, src_info, [uv_layer], 'N_N',
+ self.flip, self.rotate, self.seams)
+ if ret:
+ return {'CANCELLED'}
bmesh.update_edit_mesh(obj.data)
- if self.seams is True:
- obj.data.show_edge_seams = True
return {'FINISHED'}
diff --git a/uv_magic_uv/op/mirror_uv.py b/uv_magic_uv/op/mirror_uv.py
index f4849d18..6793ca23 100644
--- a/uv_magic_uv/op/mirror_uv.py
+++ b/uv_magic_uv/op/mirror_uv.py
@@ -20,30 +20,66 @@
__author__ = "Keith (Wahooney) Boshoff, Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
from bpy.props import (
EnumProperty,
FloatProperty,
+ BoolProperty,
)
-import bmesh
-from mathutils import Vector
-from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import mirror_uv_impl as impl
-class MUV_MirrorUV(bpy.types.Operator):
+__all__ = [
+ 'Properties',
+ 'MUV_OT_MirrorUV',
+]
+
+
+@PropertyClassRegistry()
+class Properties:
+ idname = "mirror_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_mirror_uv_enabled = BoolProperty(
+ name="Mirror UV Enabled",
+ description="Mirror UV is enabled",
+ default=False
+ )
+ scene.muv_mirror_uv_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'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_mirror_uv_enabled
+ del scene.muv_mirror_uv_axis
+
+
+@BlClassRegistry()
+class MUV_OT_MirrorUV(bpy.types.Operator):
"""
Operation class: Mirror UV
"""
- bl_idname = "uv.muv_mirror_uv"
+ bl_idname = "uv.muv_mirror_uv_operator"
bl_label = "Mirror UV"
bl_options = {'REGISTER', 'UNDO'}
- axis = EnumProperty(
+ axis: EnumProperty(
items=(
('X', "X", "Mirror Along X axis"),
('Y', "Y", "Mirror Along Y axis"),
@@ -53,7 +89,7 @@ class MUV_MirrorUV(bpy.types.Operator):
description="Mirror Axis",
default='X'
)
- error = FloatProperty(
+ error: FloatProperty(
name="Error",
description="Error threshold",
default=0.001,
@@ -63,93 +99,12 @@ class MUV_MirrorUV(bpy.types.Operator):
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)
+ def __init__(self):
+ self.__impl = impl.MirrorUVImpl()
@classmethod
def poll(cls, context):
- obj = context.active_object
- return obj and obj.type == 'MESH'
+ return impl.MirrorUVImpl.poll(context)
def execute(self, context):
- obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
-
- error = self.error
- axis = self.axis
-
- if 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'}
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/move_uv.py b/uv_magic_uv/op/move_uv.py
index a5cc401c..653918d3 100644
--- a/uv_magic_uv/op/move_uv.py
+++ b/uv_magic_uv/op/move_uv.py
@@ -20,114 +20,63 @@
__author__ = "kgeogeo, mem, Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
-import bmesh
-from mathutils import Vector
+from bpy.props import BoolProperty
+from ..impl import move_uv_impl as impl
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
-class MUV_MVUV(bpy.types.Operator):
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_MoveUV',
+]
+
+
+@PropertyClassRegistry()
+class Properties:
+ idname = "move_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_move_uv_enabled = BoolProperty(
+ name="Move UV Enabled",
+ description="Move UV is enabled",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_move_uv_enabled
+
+
+@BlClassRegistry()
+class MUV_OT_MoveUV(bpy.types.Operator):
"""
- Operator class: Move UV from View3D
+ Operator class: Move UV
"""
- bl_idname = "view3d.muv_mvuv"
- bl_label = "Move the UV from View3D"
+ bl_idname = "uv.muv_move_uv_operator"
+ bl_label = "Move UV"
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
+ self.__impl = impl.MoveUVImpl()
@classmethod
def poll(cls, context):
- return context.edit_object
+ return impl.MoveUVImpl.poll(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return impl.MoveUVImpl.is_running(_)
def modal(self, context, event):
- props = context.scene.muv_props.mvuv
- 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:
- 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
- wm = context.window_manager
- keyconfig = wm.keyconfigs.active
- select_mouse = getattr(keyconfig.preferences, "select_mouse", "LEFT")
- if 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
- props.running = False
- return {'FINISHED'}
- # confirmed
- if event.type == confirm_btn and event.value == 'PRESS':
- props.running = False
- return {'FINISHED'}
-
- return {'RUNNING_MODAL'}
+ return self.__impl.modal(self, context, event)
def execute(self, context):
- props = context.scene.muv_props.mvuv
- props.running = True
- self.__running = True
- self.__first_time = True
- context.window_manager.modal_handler_add(self)
- self.__topology_dict, self.__ini_uvs = self.__find_uv(context)
- return {'RUNNING_MODAL'}
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/texture_projection.py b/uv_magic_uv/op/texture_projection.py
deleted file mode 100644
index 77a81aa0..00000000
--- a/uv_magic_uv/op/texture_projection.py
+++ /dev/null
@@ -1,296 +0,0 @@
-# <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__ = "5.1"
-__date__ = "24 Feb 2018"
-
-from collections import namedtuple
-
-import bpy
-import bgl
-import bmesh
-import mathutils
-from bpy_extras import view3d_utils
-
-from .. import 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'}
-
- @classmethod
- def poll(cls, context):
- obj = context.active_object
- return obj is not None and obj.type == "MESH"
-
- 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 = 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 common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
-
- # get UV and texture layer
- if not bm.loops.layers.uv:
- if sc.muv_texproj_assign_uvmap:
- bm.loops.layers.uv.new()
- else:
- 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
-
- common.redraw_all_areas()
- bmesh.update_edit_mesh(obj.data)
-
- return {'FINISHED'}
diff --git a/uv_magic_uv/op/transfer_uv.py b/uv_magic_uv/op/transfer_uv.py
index 132f395e..db05b343 100644
--- a/uv_magic_uv/op/transfer_uv.py
+++ b/uv_magic_uv/op/transfer_uv.py
@@ -20,339 +20,149 @@
__author__ = "Nutti <nutti.metro@gmail.com>, Mifth, MaxRobinot"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
-
-from collections import OrderedDict
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
import bmesh
from bpy.props import BoolProperty
from .. import common
-
-
-class MUV_TransUVCopy(bpy.types.Operator):
+from ..impl import transfer_uv_impl as impl
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_TransferUV_CopyUV',
+ 'MUV_OT_TransferUV_PasteUV',
+]
+
+
+@PropertyClassRegistry()
+class Properties:
+ idname = "transfer_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ topology_copied = None
+
+ scene.muv_props.transfer_uv = Props()
+
+ scene.muv_transfer_uv_enabled = BoolProperty(
+ name="Transfer UV Enabled",
+ description="Transfer UV is enabled",
+ default=False
+ )
+ scene.muv_transfer_uv_invert_normals = BoolProperty(
+ name="Invert Normals",
+ description="Invert Normals",
+ default=False
+ )
+ scene.muv_transfer_uv_copy_seams = BoolProperty(
+ name="Copy Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_transfer_uv_enabled
+ del scene.muv_transfer_uv_invert_normals
+ del scene.muv_transfer_uv_copy_seams
+
+
+@BlClassRegistry()
+class MUV_OT_TransferUV_CopyUV(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_idname = "uv.muv_transfer_uv_operator_copy_uv"
+ bl_label = "Transfer UV Copy UV"
+ bl_description = "Transfer UV Copy UV (Topological based copy)"
bl_options = {'REGISTER', 'UNDO'}
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
+
def execute(self, context):
- props = context.scene.muv_props.transuv
- active_obj = context.scene.objects.active
+ props = context.scene.muv_props.transfer_uv
+ active_obj = context.active_object
bm = bmesh.from_edit_mesh(active_obj.data)
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
+ 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")
+ uv_layer = impl.get_uv_layer(self, bm)
+ if uv_layer is None:
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")
+ faces = impl.get_selected_src_faces(self, bm, uv_layer)
+ if faces is None:
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():
- edges = face_data[1]
- uv_loops = face_data[2]
- uvs = [l.uv.copy() for l in uv_loops]
- pin_uvs = [l.pin_uv for l in uv_loops]
- seams = [e.seam for e in edges]
- props.topology_copied.append([uvs, pin_uvs, seams])
+ props.topology_copied = faces
bmesh.update_edit_mesh(active_obj.data)
return {'FINISHED'}
-class MUV_TransUVPaste(bpy.types.Operator):
+@BlClassRegistry()
+class MUV_OT_TransferUV_PasteUV(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_idname = "uv.muv_transfer_uv_operator_paste_uv"
+ bl_label = "Transfer UV Paste UV"
+ bl_description = "Transfer UV Paste UV (Topological based paste)"
bl_options = {'REGISTER', 'UNDO'}
- invert_normals = BoolProperty(
+ invert_normals: BoolProperty(
name="Invert Normals",
description="Invert Normals",
default=False
)
- copy_seams = BoolProperty(
+ copy_seams: BoolProperty(
name="Copy Seams",
description="Copy Seams",
default=True
)
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.transfer_uv
+ if not props.topology_copied:
+ return False
+ return impl.is_valid_context(context)
+
def execute(self, context):
- props = context.scene.muv_props.transuv
- active_obj = context.scene.objects.active
+ props = context.scene.muv_props.transfer_uv
+ active_obj = context.active_object
bm = bmesh.from_edit_mesh(active_obj.data)
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
+ 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")
+ uv_layer = impl.get_uv_layer(self, bm)
+ if uv_layer is None:
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")
+ ret = impl.paste_uv(self, bm, uv_layer, props.topology_copied,
+ self.invert_normals, self.copy_seams)
+ if ret:
return {'CANCELLED'}
- # parse selection history
- for i, _ in enumerate(all_sel_faces):
- if (i == 0) or (i % 2 == 0):
- continue
- 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 j, face_data in enumerate(all_sorted_faces.values()):
- copied_data = props.topology_copied[j]
-
- # 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())[j].select = True
- self.report(
- {'WARNING'},
- "Face have different amount of vertices"
- )
- return {'FINISHED'}
-
- for k, (edge, uvloop) in enumerate(zip(face_data[1],
- face_data[2])):
- uvloop.uv = copied_data[0][k]
- uvloop.pin_uv = copied_data[1][k]
- if self.copy_seams:
- edge.seam = copied_data[2][k]
-
bmesh.update_edit_mesh(active_obj.data)
- if self.copy_seams:
- active_obj.data.show_edge_seams = True
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]
-
- 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]
-
- 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 common.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]
diff --git a/uv_magic_uv/op/uv_inspection.py b/uv_magic_uv/op/uv_inspection.py
deleted file mode 100644
index 60a754a3..00000000
--- a/uv_magic_uv/op/uv_inspection.py
+++ /dev/null
@@ -1,623 +0,0 @@
-# <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__ = "5.1"
-__date__ = "24 Feb 2018"
-
-import bpy
-import bmesh
-import bgl
-from mathutils import Vector
-
-from .. import common
-
-
-def is_polygon_same(points1, points2):
- if len(points1) != len(points2):
- return False
-
- pts1 = points1.as_list()
- pts2 = points2.as_list()
-
- for p1 in pts1:
- for p2 in pts2:
- diff = p2 - p1
- if diff.length < 0.0000001:
- pts2.remove(p2)
- break
- else:
- return False
-
- return True
-
-
-def is_segment_intersect(start1, end1, start2, end2):
- seg1 = end1 - start1
- seg2 = end2 - start2
-
- a1 = -seg1.y
- b1 = seg1.x
- d1 = -(a1 * start1.x + b1 * start1.y)
-
- a2 = -seg2.y
- b2 = seg2.x
- d2 = -(a2 * start2.x + b2 * start2.y)
-
- seg1_line2_start = a2 * start1.x + b2 * start1.y + d2
- seg1_line2_end = a2 * end1.x + b2 * end1.y + d2
-
- seg2_line1_start = a1 * start2.x + b1 * start2.y + d1
- seg2_line1_end = a1 * end2.x + b1 * end2.y + d1
-
- if (seg1_line2_start * seg1_line2_end >= 0) or \
- (seg2_line1_start * seg2_line1_end >= 0):
- return False, None
-
- u = seg1_line2_start / (seg1_line2_start - seg1_line2_end)
- out = start1 + u * seg1
-
- return True, out
-
-
-class RingBuffer:
- def __init__(self, arr):
- self.__buffer = arr.copy()
- self.__pointer = 0
-
- def __repr__(self):
- return repr(self.__buffer)
-
- def __len__(self):
- return len(self.__buffer)
-
- def insert(self, val, offset=0):
- self.__buffer.insert(self.__pointer + offset, val)
-
- def head(self):
- return self.__buffer[0]
-
- def tail(self):
- return self.__buffer[-1]
-
- def get(self, offset=0):
- size = len(self.__buffer)
- val = self.__buffer[(self.__pointer + offset) % size]
- return val
-
- def next(self):
- size = len(self.__buffer)
- self.__pointer = (self.__pointer + 1) % size
-
- def reset(self):
- self.__pointer = 0
-
- def find(self, obj):
- try:
- idx = self.__buffer.index(obj)
- except ValueError:
- return None
- return self.__buffer[idx]
-
- def find_and_next(self, obj):
- size = len(self.__buffer)
- idx = self.__buffer.index(obj)
- self.__pointer = (idx + 1) % size
-
- def find_and_set(self, obj):
- idx = self.__buffer.index(obj)
- self.__pointer = idx
-
- def as_list(self):
- return self.__buffer.copy()
-
- def reverse(self):
- self.__buffer.reverse()
- self.reset()
-
-
-# clip: reference polygon
-# subject: tested polygon
-def do_weiler_atherton_cliping(clip, subject, uv_layer, mode):
-
- clip_uvs = RingBuffer([l[uv_layer].uv.copy() for l in clip.loops])
- if is_polygon_flipped(clip_uvs):
- clip_uvs.reverse()
- subject_uvs = RingBuffer([l[uv_layer].uv.copy() for l in subject.loops])
- if is_polygon_flipped(subject_uvs):
- subject_uvs.reverse()
-
- common.debug_print("===== Clip UV List =====")
- common.debug_print(clip_uvs)
- common.debug_print("===== Subject UV List =====")
- common.debug_print(subject_uvs)
-
- # check if clip and subject is overlapped completely
- if is_polygon_same(clip_uvs, subject_uvs):
- polygons = [subject_uvs.as_list()]
- common.debug_print("===== Polygons Overlapped Completely =====")
- common.debug_print(polygons)
- return True, polygons
-
- # check if subject is in clip
- if is_points_in_polygon(subject_uvs, clip_uvs):
- polygons = [subject_uvs.as_list()]
- return True, polygons
-
- # check if clip is in subject
- if is_points_in_polygon(clip_uvs, subject_uvs):
- polygons = [subject_uvs.as_list()]
- return True, polygons
-
- # check if clip and subject is overlapped partially
- intersections = []
- while True:
- subject_uvs.reset()
- while True:
- uv_start1 = clip_uvs.get()
- uv_end1 = clip_uvs.get(1)
- uv_start2 = subject_uvs.get()
- uv_end2 = subject_uvs.get(1)
- intersected, point = is_segment_intersect(uv_start1, uv_end1,
- uv_start2, uv_end2)
- if intersected:
- clip_uvs.insert(point, 1)
- subject_uvs.insert(point, 1)
- intersections.append([point,
- [clip_uvs.get(), clip_uvs.get(1)]])
- subject_uvs.next()
- if subject_uvs.get() == subject_uvs.head():
- break
- clip_uvs.next()
- if clip_uvs.get() == clip_uvs.head():
- break
-
- common.debug_print("===== Intersection List =====")
- common.debug_print(intersections)
-
- # no intersection, so subject and clip is not overlapped
- if not intersections:
- return False, None
-
- def get_intersection_pair(intersections, key):
- for sect in intersections:
- if sect[0] == key:
- return sect[1]
-
- return None
-
- # make enter/exit pair
- subject_uvs.reset()
- subject_entering = []
- subject_exiting = []
- clip_entering = []
- clip_exiting = []
- intersect_uv_list = []
- while True:
- pair = get_intersection_pair(intersections, subject_uvs.get())
- if pair:
- sub = subject_uvs.get(1) - subject_uvs.get(-1)
- inter = pair[1] - pair[0]
- cross = sub.x * inter.y - inter.x * sub.y
- if cross < 0:
- subject_entering.append(subject_uvs.get())
- clip_exiting.append(subject_uvs.get())
- else:
- subject_exiting.append(subject_uvs.get())
- clip_entering.append(subject_uvs.get())
- intersect_uv_list.append(subject_uvs.get())
-
- subject_uvs.next()
- if subject_uvs.get() == subject_uvs.head():
- break
-
- common.debug_print("===== Enter List =====")
- common.debug_print(clip_entering)
- common.debug_print(subject_entering)
- common.debug_print("===== Exit List =====")
- common.debug_print(clip_exiting)
- common.debug_print(subject_exiting)
-
- # for now, can't handle the situation when fulfill all below conditions
- # * two faces have common edge
- # * each face is intersected
- # * Show Mode is "Part"
- # so for now, ignore this situation
- if len(subject_entering) != len(subject_exiting):
- if mode == 'FACE':
- polygons = [subject_uvs.as_list()]
- return True, polygons
- return False, None
-
- def traverse(current_list, entering, exiting, poly, current, other_list):
- result = current_list.find(current)
- if not result:
- return None
- if result != current:
- print("Internal Error")
- return None
-
- # enter
- if entering.count(current) >= 1:
- entering.remove(current)
-
- current_list.find_and_next(current)
- current = current_list.get()
-
- while exiting.count(current) == 0:
- poly.append(current.copy())
- current_list.find_and_next(current)
- current = current_list.get()
-
- # exit
- poly.append(current.copy())
- exiting.remove(current)
-
- other_list.find_and_set(current)
- return other_list.get()
-
- # Traverse
- polygons = []
- current_uv_list = subject_uvs
- other_uv_list = clip_uvs
- current_entering = subject_entering
- current_exiting = subject_exiting
-
- poly = []
- current_uv = current_entering[0]
-
- while True:
- current_uv = traverse(current_uv_list, current_entering,
- current_exiting, poly, current_uv, other_uv_list)
-
- if current_uv_list == subject_uvs:
- current_uv_list = clip_uvs
- other_uv_list = subject_uvs
- current_entering = clip_entering
- current_exiting = clip_exiting
- common.debug_print("-- Next: Clip --")
- else:
- current_uv_list = subject_uvs
- other_uv_list = clip_uvs
- current_entering = subject_entering
- current_exiting = subject_exiting
- common.debug_print("-- Next: Subject --")
-
- common.debug_print(clip_entering)
- common.debug_print(clip_exiting)
- common.debug_print(subject_entering)
- common.debug_print(subject_exiting)
-
- if not clip_entering and not clip_exiting \
- and not subject_entering and not subject_exiting:
- break
-
- polygons.append(poly)
-
- common.debug_print("===== Polygons Overlapped Partially =====")
- common.debug_print(polygons)
-
- return True, polygons
-
-
-class MUV_UVInspRenderer(bpy.types.Operator):
- """
- Operation class: Render UV Inspection
- No operation (only rendering)
- """
-
- bl_idname = "uv.muv_uvinsp_renderer"
- bl_description = "Render overlapped/flipped UVs"
- bl_label = "Overlapped/Flipped UV renderer"
-
- __handle = None
-
- @staticmethod
- def handle_add(obj, context):
- sie = bpy.types.SpaceImageEditor
- MUV_UVInspRenderer.__handle = sie.draw_handler_add(
- MUV_UVInspRenderer.draw, (obj, context), 'WINDOW', 'POST_PIXEL')
-
- @staticmethod
- def handle_remove():
- if MUV_UVInspRenderer.__handle is not None:
- bpy.types.SpaceImageEditor.draw_handler_remove(
- MUV_UVInspRenderer.__handle, 'WINDOW')
- MUV_UVInspRenderer.__handle = None
-
- @staticmethod
- def draw(_, context):
- sc = context.scene
- props = sc.muv_props.uvinsp
- prefs = context.user_preferences.addons["uv_magic_uv"].preferences
-
- # OpenGL configuration
- bgl.glEnable(bgl.GL_BLEND)
-
- # render overlapped UV
- if sc.muv_uvinsp_show_overlapped:
- color = prefs.uvinsp_overlapped_color
- for info in props.overlapped_info:
- if sc.muv_uvinsp_show_mode == 'PART':
- for poly in info["polygons"]:
- bgl.glBegin(bgl.GL_TRIANGLE_FAN)
- bgl.glColor4f(color[0], color[1], color[2], color[3])
- for uv in poly:
- x, y = context.region.view2d.view_to_region(
- uv.x, uv.y)
- bgl.glVertex2f(x, y)
- bgl.glEnd()
- elif sc.muv_uvinsp_show_mode == 'FACE':
- bgl.glBegin(bgl.GL_TRIANGLE_FAN)
- bgl.glColor4f(color[0], color[1], color[2], color[3])
- for uv in info["subject_uvs"]:
- x, y = context.region.view2d.view_to_region(uv.x, uv.y)
- bgl.glVertex2f(x, y)
- bgl.glEnd()
-
- # render flipped UV
- if sc.muv_uvinsp_show_flipped:
- color = prefs.uvinsp_flipped_color
- for info in props.flipped_info:
- if sc.muv_uvinsp_show_mode == 'PART':
- for poly in info["polygons"]:
- bgl.glBegin(bgl.GL_TRIANGLE_FAN)
- bgl.glColor4f(color[0], color[1], color[2], color[3])
- for uv in poly:
- x, y = context.region.view2d.view_to_region(
- uv.x, uv.y)
- bgl.glVertex2f(x, y)
- bgl.glEnd()
- elif sc.muv_uvinsp_show_mode == 'FACE':
- bgl.glBegin(bgl.GL_TRIANGLE_FAN)
- bgl.glColor4f(color[0], color[1], color[2], color[3])
- for uv in info["uvs"]:
- x, y = context.region.view2d.view_to_region(uv.x, uv.y)
- bgl.glVertex2f(x, y)
- bgl.glEnd()
-
-
-def is_polygon_flipped(points):
- area = 0.0
- for i in range(len(points)):
- uv1 = points.get(i)
- uv2 = points.get(i + 1)
- a = uv1.x * uv2.y - uv1.y * uv2.x
- area = area + a
- if area < 0:
- # clock-wise
- return True
- return False
-
-
-def is_point_in_polygon(point, subject_points):
- count = 0
- for i in range(len(subject_points)):
- uv_start1 = subject_points.get(i)
- uv_end1 = subject_points.get(i + 1)
- uv_start2 = point
- uv_end2 = Vector((1000000.0, point.y))
- intersected, _ = is_segment_intersect(uv_start1, uv_end1,
- uv_start2, uv_end2)
- if intersected:
- count = count + 1
-
- return count % 2
-
-
-def is_points_in_polygon(points, subject_points):
- for i in range(len(points)):
- internal = is_point_in_polygon(points.get(i), subject_points)
- if not internal:
- return False
-
- return True
-
-
-def get_overlapped_uv_info(bm, faces, uv_layer, mode):
- # at first, check island overlapped
- isl = common.get_island_info_from_faces(bm, faces, uv_layer)
- overlapped_isl_pairs = []
- for i, i1 in enumerate(isl):
- for i2 in isl[i + 1:]:
- if (i1["max"].x < i2["min"].x) or (i2["max"].x < i1["min"].x) or \
- (i1["max"].y < i2["min"].y) or (i2["max"].y < i1["min"].y):
- continue
- overlapped_isl_pairs.append([i1, i2])
-
- # next, check polygon overlapped
- overlapped_uvs = []
- for oip in overlapped_isl_pairs:
- for clip in oip[0]["faces"]:
- f_clip = clip["face"]
- for subject in oip[1]["faces"]:
- f_subject = subject["face"]
-
- # fast operation, apply bounding box algorithm
- if (clip["max_uv"].x < subject["min_uv"].x) or \
- (subject["max_uv"].x < clip["min_uv"].x) or \
- (clip["max_uv"].y < subject["min_uv"].y) or \
- (subject["max_uv"].y < clip["min_uv"].y):
- continue
-
- # slow operation, apply Weiler-Atherton cliping algorithm
- result, polygons = do_weiler_atherton_cliping(f_clip,
- f_subject,
- uv_layer, mode)
- if result:
- subject_uvs = [l[uv_layer].uv.copy()
- for l in f_subject.loops]
- overlapped_uvs.append({"clip_face": f_clip,
- "subject_face": f_subject,
- "subject_uvs": subject_uvs,
- "polygons": polygons})
-
- return overlapped_uvs
-
-
-def get_flipped_uv_info(faces, uv_layer):
- flipped_uvs = []
- for f in faces:
- polygon = RingBuffer([l[uv_layer].uv.copy() for l in f.loops])
- if is_polygon_flipped(polygon):
- uvs = [l[uv_layer].uv.copy() for l in f.loops]
- flipped_uvs.append({"face": f, "uvs": uvs,
- "polygons": [polygon.as_list()]})
-
- return flipped_uvs
-
-
-def update_uvinsp_info(context):
- sc = context.scene
- props = sc.muv_props.uvinsp
-
- obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
- uv_layer = bm.loops.layers.uv.verify()
-
- if context.tool_settings.use_uv_select_sync:
- sel_faces = [f for f in bm.faces]
- else:
- sel_faces = [f for f in bm.faces if f.select]
- props.overlapped_info = get_overlapped_uv_info(bm, sel_faces, uv_layer,
- sc.muv_uvinsp_show_mode)
- props.flipped_info = get_flipped_uv_info(sel_faces, uv_layer)
-
-
-class MUV_UVInspUpdate(bpy.types.Operator):
- """
- Operation class: Update
- """
-
- bl_idname = "uv.muv_uvinsp_update"
- bl_label = "Update"
- bl_description = "Update Overlapped/Flipped UV"
- bl_options = {'REGISTER', 'UNDO'}
-
- def execute(self, context):
- update_uvinsp_info(context)
-
- if context.area:
- context.area.tag_redraw()
-
- return {'FINISHED'}
-
-
-class MUV_UVInspDisplay(bpy.types.Operator):
- """
- Operation class: Display
- """
-
- bl_idname = "uv.muv_uvinsp_display"
- bl_label = "Display"
- bl_description = "Display Overlapped/Flipped UV"
- bl_options = {'REGISTER', 'UNDO'}
-
- def execute(self, context):
- sc = context.scene
- props = sc.muv_props.uvinsp
- if not props.display_running:
- update_uvinsp_info(context)
- MUV_UVInspRenderer.handle_add(self, context)
- props.display_running = True
- else:
- MUV_UVInspRenderer.handle_remove()
- props.display_running = False
-
- if context.area:
- context.area.tag_redraw()
-
- return {'FINISHED'}
-
-
-class MUV_UVInspSelectOverlapped(bpy.types.Operator):
- """
- Operation class: Select faces which have overlapped UVs
- """
-
- bl_idname = "uv.muv_uvinsp_select_overlapped"
- bl_label = "Overlapped"
- bl_description = "Select faces which have overlapped UVs"
- bl_options = {'REGISTER', 'UNDO'}
-
- def execute(self, context):
- obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
- uv_layer = bm.loops.layers.uv.verify()
-
- if context.tool_settings.use_uv_select_sync:
- sel_faces = [f for f in bm.faces]
- else:
- sel_faces = [f for f in bm.faces if f.select]
-
- overlapped_info = get_overlapped_uv_info(bm, sel_faces, uv_layer,
- 'FACE')
-
- for info in overlapped_info:
- if context.tool_settings.use_uv_select_sync:
- info["subject_face"].select = True
- else:
- for l in info["subject_face"].loops:
- l[uv_layer].select = True
-
- bmesh.update_edit_mesh(obj.data)
-
- return {'FINISHED'}
-
-
-class MUV_UVInspSelectFlipped(bpy.types.Operator):
- """
- Operation class: Select faces which have flipped UVs
- """
-
- bl_idname = "uv.muv_uvinsp_select_flipped"
- bl_label = "Flipped"
- bl_description = "Select faces which have flipped UVs"
- bl_options = {'REGISTER', 'UNDO'}
-
- def execute(self, context):
- obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- if common.check_version(2, 73, 0) >= 0:
- bm.faces.ensure_lookup_table()
- uv_layer = bm.loops.layers.uv.verify()
-
- if context.tool_settings.use_uv_select_sync:
- sel_faces = [f for f in bm.faces]
- else:
- sel_faces = [f for f in bm.faces if f.select]
-
- flipped_info = get_flipped_uv_info(sel_faces, uv_layer)
-
- for info in flipped_info:
- if context.tool_settings.use_uv_select_sync:
- info["face"].select = True
- else:
- for l in info["face"].loops:
- l[uv_layer].select = True
-
- bmesh.update_edit_mesh(obj.data)
-
- return {'FINISHED'}
diff --git a/uv_magic_uv/op/uvw.py b/uv_magic_uv/op/uvw.py
index 10202677..c97e0e04 100644
--- a/uv_magic_uv/op/uvw.py
+++ b/uv_magic_uv/op/uvw.py
@@ -20,10 +20,8 @@
__author__ = "Alexander Milovsky, Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
-
-from math import sin, cos, pi
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
import bmesh
@@ -32,37 +30,70 @@ from bpy.props import (
FloatVectorProperty,
BoolProperty
)
-from mathutils import Vector
from .. import common
+from ..impl import uvw_impl as impl
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_UVW_BoxMap',
+ 'MUV_OT_UVW_BestPlanerMap',
+]
+
+
+@PropertyClassRegistry()
+class Properties:
+ idname = "uvw"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_uvw_enabled = BoolProperty(
+ name="UVW Enabled",
+ description="UVW is enabled",
+ default=False
+ )
+ scene.muv_uvw_assign_uvmap = BoolProperty(
+ name="Assign UVMap",
+ description="Assign UVMap when no UVmaps are available",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_uvw_enabled
+ del scene.muv_uvw_assign_uvmap
-class MUV_UVWBoxMap(bpy.types.Operator):
- bl_idname = "uv.muv_uvw_box_map"
+@BlClassRegistry()
+class MUV_OT_UVW_BoxMap(bpy.types.Operator):
+ bl_idname = "uv.muv_uvw_operator_box_map"
bl_label = "Box Map"
bl_options = {'REGISTER', 'UNDO'}
- size = FloatProperty(
+ size: FloatProperty(
name="Size",
default=1.0,
precision=4
)
- rotation = FloatVectorProperty(
+ rotation: FloatVectorProperty(
name="XYZ Rotation",
size=3,
default=(0.0, 0.0, 0.0)
)
- offset = FloatVectorProperty(
+ offset: FloatVectorProperty(
name="XYZ Offset",
size=3,
default=(0.0, 0.0, 0.0)
)
- tex_aspect = FloatProperty(
+ tex_aspect: FloatProperty(
name="Texture Aspect",
default=1.0,
precision=4
)
- assign_uvmap = BoolProperty(
+ assign_uvmap: BoolProperty(
name="Assign UVMap",
description="Assign UVMap when no UVmaps are available",
default=True
@@ -70,8 +101,10 @@ class MUV_UVWBoxMap(bpy.types.Operator):
@classmethod
def poll(cls, context):
- obj = context.active_object
- return obj and obj.type == 'MESH'
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
def execute(self, context):
obj = context.active_object
@@ -80,102 +113,43 @@ class MUV_UVWBoxMap(bpy.types.Operator):
bm.faces.ensure_lookup_table()
# get UV layer
- if not bm.loops.layers.uv:
- if self.assign_uvmap:
- bm.loops.layers.uv.new()
- else:
- self.report(
- {'WARNING'}, "Object must have more than one UV map")
- return {'CANCELLED'}
- uv_layer = bm.loops.layers.uv.verify()
-
- scale = 1.0 / self.size
-
- sx = 1.0 * scale
- sy = 1.0 * scale
- sz = 1.0 * scale
- ofx = self.offset[0]
- ofy = self.offset[1]
- ofz = self.offset[2]
- rx = self.rotation[0] * pi / 180.0
- ry = self.rotation[1] * pi / 180.0
- rz = self.rotation[2] * pi / 180.0
- aspect = self.tex_aspect
-
- sel_faces = [f for f in bm.faces if f.select]
-
- # update UV coordinate
- for f in sel_faces:
- n = f.normal
- for l in f.loops:
- co = l.vert.co
- x = co.x * sx
- y = co.y * sy
- z = co.z * sz
-
- # X-plane
- if abs(n[0]) >= abs(n[1]) and abs(n[0]) >= abs(n[2]):
- if n[0] >= 0.0:
- u = (y - ofy) * cos(rx) + (z - ofz) * sin(rx)
- v = -(y * aspect - ofy) * sin(rx) +\
- (z * aspect - ofz) * cos(rx)
- else:
- u = -(y - ofy) * cos(rx) + (z - ofz) * sin(rx)
- v = (y * aspect - ofy) * sin(rx) +\
- (z * aspect - ofz) * cos(rx)
- # Y-plane
- elif abs(n[1]) >= abs(n[0]) and abs(n[1]) >= abs(n[2]):
- if n[1] >= 0.0:
- u = -(x - ofx) * cos(ry) + (z - ofz) * sin(ry)
- v = (x * aspect - ofx) * sin(ry) +\
- (z * aspect - ofz) * cos(ry)
- else:
- u = (x - ofx) * cos(ry) + (z - ofz) * sin(ry)
- v = -(x * aspect - ofx) * sin(ry) +\
- (z * aspect - ofz) * cos(ry)
- # Z-plane
- else:
- if n[2] >= 0.0:
- u = (x - ofx) * cos(rz) + (y - ofy) * sin(rz)
- v = -(x * aspect - ofx) * sin(rz) +\
- (y * aspect - ofy) * cos(rz)
- else:
- u = -(x - ofx) * cos(rz) - (y + ofy) * sin(rz)
- v = -(x * aspect + ofx) * sin(rz) +\
- (y * aspect - ofy) * cos(rz)
-
- l[uv_layer].uv = Vector((u, v))
+ uv_layer = impl.get_uv_layer(self, bm, self.assign_uvmap)
+ if not uv_layer:
+ return {'CANCELLED'}
+ impl.apply_box_map(bm, uv_layer, self.size, self.offset,
+ self.rotation, self.tex_aspect)
bmesh.update_edit_mesh(obj.data)
return {'FINISHED'}
-class MUV_UVWBestPlanerMap(bpy.types.Operator):
- bl_idname = "uv.muv_uvw_best_planer_map"
+@BlClassRegistry()
+class MUV_OT_UVW_BestPlanerMap(bpy.types.Operator):
+ bl_idname = "uv.muv_uvw_operator_best_planer_map"
bl_label = "Best Planer Map"
bl_options = {'REGISTER', 'UNDO'}
- size = FloatProperty(
+ size: FloatProperty(
name="Size",
default=1.0,
precision=4
)
- rotation = FloatProperty(
+ rotation: FloatProperty(
name="XY Rotation",
default=0.0
)
- offset = FloatVectorProperty(
+ offset: FloatVectorProperty(
name="XY Offset",
size=2,
default=(0.0, 0.0)
)
- tex_aspect = FloatProperty(
+ tex_aspect: FloatProperty(
name="Texture Aspect",
default=1.0,
precision=4
)
- assign_uvmap = BoolProperty(
+ assign_uvmap: BoolProperty(
name="Assign UVMap",
description="Assign UVMap when no UVmaps are available",
default=True
@@ -183,8 +157,10 @@ class MUV_UVWBestPlanerMap(bpy.types.Operator):
@classmethod
def poll(cls, context):
- obj = context.active_object
- return obj and obj.type == 'MESH'
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return impl.is_valid_context(context)
def execute(self, context):
obj = context.active_object
@@ -193,44 +169,12 @@ class MUV_UVWBestPlanerMap(bpy.types.Operator):
bm.faces.ensure_lookup_table()
# get UV layer
- if not bm.loops.layers.uv:
- if self.assign_uvmap:
- bm.loops.layers.uv.new()
- else:
- self.report(
- {'WARNING'}, "Object must have more than one UV map")
- return {'CANCELLED'}
-
- uv_layer = bm.loops.layers.uv.verify()
-
- scale = 1.0 / self.size
-
- sx = 1.0 * scale
- sy = 1.0 * scale
- ofx = self.offset[0]
- ofy = self.offset[1]
- rz = self.rotation * pi / 180.0
- aspect = self.tex_aspect
-
- sel_faces = [f for f in bm.faces if f.select]
-
- # calculate average of normal
- n_ave = Vector((0.0, 0.0, 0.0))
- for f in sel_faces:
- n_ave = n_ave + f.normal
- q = n_ave.rotation_difference(Vector((0.0, 0.0, 1.0)))
-
- # update UV coordinate
- for f in sel_faces:
- for l in f.loops:
- co = q * l.vert.co
- x = co.x * sx
- y = co.y * sy
-
- u = x * cos(rz) - y * sin(rz) + ofx
- v = -x * aspect * sin(rz) - y * aspect * cos(rz) + ofy
-
- l[uv_layer].uv = Vector((u, v))
+ uv_layer = impl.get_uv_layer(self, bm, self.assign_uvmap)
+ if not uv_layer:
+ return {'CANCELLED'}
+
+ impl.apply_planer_map(bm, uv_layer, self.size, self.offset,
+ self.rotation, self.tex_aspect)
bmesh.update_edit_mesh(obj.data)
diff --git a/uv_magic_uv/op/world_scale_uv.py b/uv_magic_uv/op/world_scale_uv.py
deleted file mode 100644
index e256fbac..00000000
--- a/uv_magic_uv/op/world_scale_uv.py
+++ /dev/null
@@ -1,236 +0,0 @@
-# <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__ = "5.1"
-__date__ = "24 Feb 2018"
-
-from math import sqrt
-
-import bpy
-import bmesh
-from mathutils import Vector
-from bpy.props import EnumProperty
-
-from .. import common
-
-
-def measure_wsuv_info(obj):
- mesh_area = common.measure_mesh_area(obj)
- uv_area = common.measure_uv_area(obj)
-
- if not uv_area:
- return None, None, None
-
- if mesh_area == 0.0:
- density = 0.0
- else:
- density = sqrt(uv_area) / sqrt(mesh_area)
-
- return uv_area, mesh_area, density
-
-
-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):
- sc = context.scene
- obj = context.active_object
-
- uv_area, mesh_area, density = measure_wsuv_info(obj)
- if not uv_area:
- self.report({'WARNING'},
- "Object must have more than one UV map and texture")
- return {'CANCELLED'}
-
- sc.muv_wsuv_src_uv_area = uv_area
- sc.muv_wsuv_src_mesh_area = mesh_area
- sc.muv_wsuv_src_density = density
-
- self.report({'INFO'},
- "UV Area: {0}, Mesh Area: {1}, Texel Density: {2}"
- .format(uv_area, mesh_area, density))
-
- 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'}
-
- origin = EnumProperty(
- name="Origin",
- description="Aspect Origin",
- items=[
- ('CENTER', 'Center', 'Center'),
- ('LEFT_TOP', 'Left Top', 'Left Bottom'),
- ('LEFT_CENTER', 'Left Center', 'Left Center'),
- ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'),
- ('CENTER_TOP', 'Center Top', 'Center Top'),
- ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'),
- ('RIGHT_TOP', 'Right Top', 'Right Top'),
- ('RIGHT_CENTER', 'Right Center', 'Right Center'),
- ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom')
-
- ],
- default="CENTER"
- )
-
- def draw(self, _):
- layout = self.layout
-
- layout.prop(self, "origin")
-
- def execute(self, context):
- sc = context.scene
- obj = context.active_object
- bm = bmesh.from_edit_mesh(obj.data)
- if common.check_version(2, 73, 0) >= 0:
- bm.verts.ensure_lookup_table()
- bm.edges.ensure_lookup_table()
- bm.faces.ensure_lookup_table()
-
- sel_faces = [f for f in bm.faces if f.select]
-
- uv_area, mesh_area, density = measure_wsuv_info(obj)
- if not uv_area:
- self.report({'WARNING'},
- "Object must have more than one UV map and texture")
- return {'CANCELLED'}
-
- uv_layer = bm.loops.layers.uv.verify()
-
- if sc.muv_wsuv_mode == 'PROPORTIONAL':
- tgt_density = sc.muv_wsuv_src_density * sqrt(mesh_area) / \
- sqrt(sc.muv_wsuv_src_mesh_area)
- elif sc.muv_wsuv_mode == 'SCALING':
- tgt_density = sc.muv_wsuv_src_density * sc.muv_wsuv_scaling_factor
- elif sc.muv_wsuv_mode == 'USER':
- tgt_density = sc.muv_wsuv_tgt_density
- elif sc.muv_wsuv_mode == 'CONSTANT':
- tgt_density = sc.muv_wsuv_src_density
-
- factor = tgt_density / density
-
- # calculate origin
- if self.origin == 'CENTER':
- origin = Vector((0.0, 0.0))
- num = 0
- for f in sel_faces:
- for l in f.loops:
- uv = l[uv_layer].uv
- origin = origin + uv
- num = num + 1
- origin = origin / num
- elif self.origin == 'LEFT_TOP':
- origin = Vector((100000.0, -100000.0))
- for f in sel_faces:
- for l in f.loops:
- uv = l[uv_layer].uv
- origin.x = min(origin.x, uv.x)
- origin.y = max(origin.y, uv.y)
- elif self.origin == 'LEFT_CENTER':
- origin = Vector((100000.0, 0.0))
- num = 0
- for f in sel_faces:
- for l in f.loops:
- uv = l[uv_layer].uv
- origin.x = min(origin.x, uv.x)
- origin.y = origin.y + uv.y
- num = num + 1
- origin.y = origin.y / num
- elif self.origin == 'LEFT_BOTTOM':
- origin = Vector((100000.0, 100000.0))
- for f in sel_faces:
- for l in f.loops:
- uv = l[uv_layer].uv
- origin.x = min(origin.x, uv.x)
- origin.y = min(origin.y, uv.y)
- elif self.origin == 'CENTER_TOP':
- origin = Vector((0.0, -100000.0))
- num = 0
- for f in sel_faces:
- for l in f.loops:
- uv = l[uv_layer].uv
- origin.x = origin.x + uv.x
- origin.y = max(origin.y, uv.y)
- num = num + 1
- origin.x = origin.x / num
- elif self.origin == 'CENTER_BOTTOM':
- origin = Vector((0.0, 100000.0))
- num = 0
- for f in sel_faces:
- for l in f.loops:
- uv = l[uv_layer].uv
- origin.x = origin.x + uv.x
- origin.y = min(origin.y, uv.y)
- num = num + 1
- origin.x = origin.x / num
- elif self.origin == 'RIGHT_TOP':
- origin = Vector((-100000.0, -100000.0))
- for f in sel_faces:
- for l in f.loops:
- uv = l[uv_layer].uv
- origin.x = max(origin.x, uv.x)
- origin.y = max(origin.y, uv.y)
- elif self.origin == 'RIGHT_CENTER':
- origin = Vector((-100000.0, 0.0))
- num = 0
- for f in sel_faces:
- for l in f.loops:
- uv = l[uv_layer].uv
- origin.x = max(origin.x, uv.x)
- origin.y = origin.y + uv.y
- num = num + 1
- origin.y = origin.y / num
- elif self.origin == 'RIGHT_BOTTOM':
- origin = Vector((-100000.0, 100000.0))
- for f in sel_faces:
- for l in f.loops:
- uv = l[uv_layer].uv
- origin.x = max(origin.x, uv.x)
- origin.y = min(origin.y, uv.y)
-
- # update UV coordinate
- for f in sel_faces:
- for l in f.loops:
- uv = l[uv_layer].uv
- diff = uv - origin
- l[uv_layer].uv = origin + diff * factor
-
- bmesh.update_edit_mesh(obj.data)
-
- self.report({'INFO'}, "Scaling factor: {0}".format(factor))
-
- return {'FINISHED'}
diff --git a/uv_magic_uv/preferences.py b/uv_magic_uv/preferences.py
index d8cdf86b..3ba94376 100644
--- a/uv_magic_uv/preferences.py
+++ b/uv_magic_uv/preferences.py
@@ -20,23 +20,114 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+import bpy
from bpy.props import (
FloatProperty,
FloatVectorProperty,
+ BoolProperty,
+ EnumProperty,
+ IntProperty,
)
from bpy.types import AddonPreferences
+from . import op
+from . import ui
+from . import addon_updater_ops
-class MUV_Preferences(AddonPreferences):
+__all__ = [
+ 'add_builtin_menu',
+ 'remove_builtin_menu',
+ 'Preferences'
+]
+
+
+def view3d_uvmap_menu_fn(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.separator()
+ layout.label(text="Copy/Paste UV", icon='IMAGE')
+ # Copy/Paste UV
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_CopyPasteUV.bl_idname,
+ text="Copy/Paste UV")
+ # Transfer UV
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TransferUV.bl_idname,
+ text="Transfer UV")
+
+ layout.separator()
+ layout.label(text="UV Manipulation", icon='IMAGE')
+ # Flip/Rotate UV
+ ops = layout.operator(op.flip_rotate_uv.MUV_OT_FlipRotate.bl_idname,
+ text="Flip/Rotate UV")
+ ops.seams = sc.muv_flip_rotate_uv_seams
+ # Mirror UV
+ ops = layout.operator(op.mirror_uv.MUV_OT_MirrorUV.bl_idname,
+ text="Mirror UV")
+ ops.axis = sc.muv_mirror_uv_axis
+ # Move UV
+ layout.operator(op.move_uv.MUV_OT_MoveUV.bl_idname, text="Move UV")
+
+ layout.separator()
+ # UVW
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_UVW.bl_idname, text="UVW")
+
+
+def view3d_object_menu_fn(self, _):
+ layout = self.layout
+
+ layout.separator()
+ layout.label(text="Copy/Paste UV", icon='IMAGE')
+ # Copy/Paste UV (Among Objecct)
+ layout.menu(ui.VIEW3D_MT_object.MUV_MT_CopyPasteUV_Object.bl_idname,
+ text="Copy/Paste UV")
+
+
+def image_uvs_menu_fn(self, _):
+ layout = self.layout
+
+ layout.separator()
+ # Copy/Paste UV (on UV/Image Editor)
+ layout.label(text="Copy/Paste UV", icon='IMAGE')
+ layout.menu(ui.IMAGE_MT_uvs.MUV_MT_CopyPasteUV_UVEdit.bl_idname,
+ text="Copy/Paste UV")
+
+
+def add_builtin_menu():
+ bpy.types.VIEW3D_MT_uv_map.append(view3d_uvmap_menu_fn)
+ bpy.types.VIEW3D_MT_object.append(view3d_object_menu_fn)
+ bpy.types.IMAGE_MT_uvs.append(image_uvs_menu_fn)
+
+
+def remove_builtin_menu():
+ bpy.types.IMAGE_MT_uvs.remove(image_uvs_menu_fn)
+ bpy.types.VIEW3D_MT_object.append(view3d_object_menu_fn)
+ bpy.types.VIEW3D_MT_uv_map.remove(view3d_uvmap_menu_fn)
+
+
+class Preferences(AddonPreferences):
"""Preferences class: Preferences for this add-on"""
- bl_idname = __package__
+ bl_idname = "uv_magic_uv"
+
+ def update_enable_builtin_menu(self, _):
+ if self['enable_builtin_menu']:
+ add_builtin_menu()
+ else:
+ remove_builtin_menu()
+
+ # enable to add features to built-in menu
+ enable_builtin_menu = BoolProperty(
+ name="Built-in Menu",
+ description="Enable built-in menu",
+ default=True,
+ update=update_enable_builtin_menu
+ )
# for UV Sculpt
- uvsculpt_brush_color = FloatVectorProperty(
+ uv_sculpt_brush_color = FloatVectorProperty(
name="Color",
description="Color",
default=(1.0, 0.4, 0.4, 1.0),
@@ -47,7 +138,7 @@ class MUV_Preferences(AddonPreferences):
)
# for Overlapped UV
- uvinsp_overlapped_color = FloatVectorProperty(
+ uv_inspection_overlapped_color = FloatVectorProperty(
name="Color",
description="Color",
default=(0.0, 0.0, 1.0, 0.3),
@@ -58,7 +149,7 @@ class MUV_Preferences(AddonPreferences):
)
# for Flipped UV
- uvinsp_flipped_color = FloatVectorProperty(
+ uv_inspection_flipped_color = FloatVectorProperty(
name="Color",
description="Color",
default=(1.0, 0.0, 0.0, 0.3),
@@ -69,7 +160,7 @@ class MUV_Preferences(AddonPreferences):
)
# for Texture Projection
- texproj_canvas_padding = FloatVectorProperty(
+ texture_projection_canvas_padding = FloatVectorProperty(
name="Canvas Padding",
description="Canvas Padding",
size=2,
@@ -78,139 +169,245 @@ class MUV_Preferences(AddonPreferences):
default=(20.0, 20.0))
# for UV Bounding Box
- uvbb_cp_size = FloatProperty(
+ uv_bounding_box_cp_size = FloatProperty(
name="Size",
description="Control Point Size",
default=6.0,
min=3.0,
max=100.0)
- uvbb_cp_react_size = FloatProperty(
+ uv_bounding_box_cp_react_size = FloatProperty(
name="React Size",
description="Size event fired",
default=10.0,
min=3.0,
max=100.0)
- def draw(self, _):
+ # for UI
+ category = EnumProperty(
+ name="Category",
+ description="Preferences Category",
+ items=[
+ ('INFO', "Information", "Information about this add-on"),
+ ('CONFIG', "Configuration", "Configuration about this add-on"),
+ ('UPDATE', "Update", "Update this add-on"),
+ ],
+ default='INFO'
+ )
+ info_desc_expanded = BoolProperty(
+ name="Description",
+ description="Description",
+ default=False
+ )
+ info_loc_expanded = BoolProperty(
+ name="Location",
+ description="Location",
+ default=False
+ )
+ conf_uv_sculpt_expanded = BoolProperty(
+ name="UV Sculpt",
+ description="UV Sculpt",
+ default=False
+ )
+ conf_uv_inspection_expanded = BoolProperty(
+ name="UV Inspection",
+ description="UV Inspection",
+ default=False
+ )
+ conf_texture_projection_expanded = BoolProperty(
+ name="Texture Projection",
+ description="Texture Projection",
+ default=False
+ )
+ conf_uv_bounding_box_expanded = BoolProperty(
+ name="UV Bounding Box",
+ description="UV Bounding Box",
+ default=False
+ )
+
+ # for add-on updater
+ auto_check_update = BoolProperty(
+ name="Auto-check for Update",
+ description="If enabled, auto-check for updates using an interval",
+ default=False
+ )
+ updater_intrval_months = IntProperty(
+ name='Months',
+ description="Number of months between checking for updates",
+ default=0,
+ min=0
+ )
+ updater_intrval_days = IntProperty(
+ name='Days',
+ description="Number of days between checking for updates",
+ default=7,
+ min=0
+ )
+ updater_intrval_hours = IntProperty(
+ name='Hours',
+ description="Number of hours between checking for updates",
+ default=0,
+ min=0,
+ max=23
+ )
+ updater_intrval_minutes = IntProperty(
+ name='Minutes',
+ description="Number of minutes between checking for updates",
+ default=0,
+ min=0,
+ max=59
+ )
+
+ def draw(self, context):
layout = self.layout
- layout.label("[Configuration]")
-
- layout.label("UV Sculpt:")
- sp = layout.split(percentage=0.05)
- col = sp.column() # spacer
- sp = sp.split(percentage=0.3)
- col = sp.column()
- col.label("Brush Color:")
- col.prop(self, "uvsculpt_brush_color", text="")
-
- layout.separator()
-
- layout.label("UV Inspection:")
- sp = layout.split(percentage=0.05)
- col = sp.column() # spacer
- sp = sp.split(percentage=0.3)
- col = sp.column()
- col.label("Overlapped UV Color:")
- col.prop(self, "uvinsp_overlapped_color", text="")
- sp = sp.split(percentage=0.45)
- col = sp.column()
- col.label("Flipped UV Color:")
- col.prop(self, "uvinsp_flipped_color", text="")
-
- layout.separator()
-
- layout.label("Texture Projection:")
- sp = layout.split(percentage=0.05)
- col = sp.column() # spacer
- sp = sp.split(percentage=0.3)
- col = sp.column()
- col.prop(self, "texproj_canvas_padding")
-
- layout.separator()
-
- layout.label("UV Bounding Box:")
- 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("--------------------------------------")
-
- 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 are new to this add-on.")
- column.label("https://github.com/nutti/Magic-UV/wiki/Tutorial")
-
- layout.label("--------------------------------------")
-
- layout.label("[Location]")
-
- row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("3D View > Tool shelf > Copy/Paste UV (Object mode)")
- sp = sp.split(percentage=1.0)
- col = sp.column(align=True)
- col.label("Copy/Paste UV (Among objects)")
-
- row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("3D View > Tool shelf > Copy/Paste UV (Edit mode)")
- sp = sp.split(percentage=1.0)
- col = sp.column(align=True)
- col.label("Copy/Paste UV (Among faces in 3D View)")
- col.label("Transfer UV")
-
- row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("3D View > Tool shelf > UV Manipulation (Edit mode)")
- sp = sp.split(percentage=1.0)
- col = sp.column(align=True)
- col.label("Flip/Rotate UV")
- col.label("Mirror UV")
- col.label("Move UV")
- col.label("World Scale UV")
- col.label("Preserve UV Aspect")
- col.label("Texture Lock")
- col.label("Texture Wrap")
- col.label("UV Sculpt")
-
- row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("3D View > Tool shelf > UV Manipulation (Edit mode)")
- sp = sp.split(percentage=1.0)
- col = sp.column(align=True)
- col.label("Unwrap Constraint")
- col.label("Texture Projection")
- col.label("UVW")
-
- row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("UV/Image Editor > Tool shelf > Copy/Paste UV")
- sp = sp.split(percentage=1.0)
- col = sp.column(align=True)
- col.label("Copy/Paste UV (Among faces in UV/Image Editor)")
-
- row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("UV/Image Editor > Tool shelf > UV Manipulation")
- sp = sp.split(percentage=1.0)
- col = sp.column(align=True)
- col.label("Align UV")
- col.label("Smooth UV")
- col.label("Select UV")
- col.label("Pack UV (Extension)")
-
- row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("UV/Image Editor > Tool shelf > Editor Enhancement")
- sp = sp.split(percentage=1.0)
- col = sp.column(align=True)
- col.label("Align UV Cursor")
- col.label("UV Cursor Location")
- col.label("UV Bounding Box")
- col.label("UV Inspection")
+ layout.row().prop(self, "category", expand=True)
+
+ if self.category == 'INFO':
+ layout.prop(
+ self, "info_desc_expanded", text="Description",
+ icon='DISCLOSURE_TRI_DOWN' if self.info_desc_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.info_desc_expanded:
+ column = layout.column(align=True)
+ column.label("Magic UV is composed of many UV editing" +
+ " features.")
+ column.label("See tutorial page if you are new to this" +
+ " add-on.")
+ column.label("https://github.com/nutti/Magic-UV/wiki/Tutorial")
+
+ layout.prop(
+ self, "info_loc_expanded", text="Location",
+ icon='DISCLOSURE_TRI_DOWN' if self.info_loc_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.info_loc_expanded:
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("3D View > Tool shelf > Copy/Paste UV (Object mode)")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Copy/Paste UV (Among objects)")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("3D View > Tool shelf > Copy/Paste UV (Edit mode)")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Copy/Paste UV (Among faces in 3D View)")
+ col.label("Transfer UV")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("3D View > Tool shelf > UV Manipulation (Edit mode)")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Flip/Rotate UV")
+ col.label("Mirror UV")
+ col.label("Move UV")
+ col.label("World Scale UV")
+ col.label("Preserve UV Aspect")
+ col.label("Texture Lock")
+ col.label("Texture Wrap")
+ col.label("UV Sculpt")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("3D View > Tool shelf > UV Manipulation (Edit mode)")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Unwrap Constraint")
+ col.label("Texture Projection")
+ col.label("UVW")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("UV/Image Editor > Tool shelf > Copy/Paste UV")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Copy/Paste UV (Among faces in UV/Image Editor)")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("UV/Image Editor > Tool shelf > UV Manipulation")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Align UV")
+ col.label("Smooth UV")
+ col.label("Select UV")
+ col.label("Pack UV (Extension)")
+
+ row = layout.row(align=True)
+ sp = row.split(percentage=0.5)
+ sp.label("UV/Image Editor > Tool shelf > Editor Enhancement")
+ sp = sp.split(percentage=1.0)
+ col = sp.column(align=True)
+ col.label("Align UV Cursor")
+ col.label("UV Cursor Location")
+ col.label("UV Bounding Box")
+ col.label("UV Inspection")
+
+ elif self.category == 'CONFIG':
+ layout.prop(self, "enable_builtin_menu", text="Built-in Menu")
+
+ layout.separator()
+
+ layout.prop(
+ self, "conf_uv_sculpt_expanded", text="UV Sculpt",
+ icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_sculpt_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_uv_sculpt_expanded:
+ sp = layout.split(percentage=0.05)
+ col = sp.column() # spacer
+ sp = sp.split(percentage=0.3)
+ col = sp.column()
+ col.label("Brush Color:")
+ col.prop(self, "uv_sculpt_brush_color", text="")
+ layout.separator()
+
+ layout.prop(
+ self, "conf_uv_inspection_expanded", text="UV Inspection",
+ icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_inspection_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_uv_inspection_expanded:
+ sp = layout.split(percentage=0.05)
+ col = sp.column() # spacer
+ sp = sp.split(percentage=0.3)
+ col = sp.column()
+ col.label("Overlapped UV Color:")
+ col.prop(self, "uv_inspection_overlapped_color", text="")
+ sp = sp.split(percentage=0.45)
+ col = sp.column()
+ col.label("Flipped UV Color:")
+ col.prop(self, "uv_inspection_flipped_color", text="")
+ layout.separator()
+
+ layout.prop(
+ self, "conf_texture_projection_expanded",
+ text="Texture Projection",
+ icon='DISCLOSURE_TRI_DOWN'
+ if self.conf_texture_projection_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_texture_projection_expanded:
+ sp = layout.split(percentage=0.05)
+ col = sp.column() # spacer
+ sp = sp.split(percentage=0.3)
+ col = sp.column()
+ col.prop(self, "texture_projection_canvas_padding")
+ layout.separator()
+
+ layout.prop(
+ self, "conf_uv_bounding_box_expanded", text="UV Bounding Box",
+ icon='DISCLOSURE_TRI_DOWN'
+ if self.conf_uv_bounding_box_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_uv_bounding_box_expanded:
+ 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, "uv_bounding_box_cp_size")
+ col.prop(self, "uv_bounding_box_cp_react_size")
+ layout.separator()
+
+ elif self.category == 'UPDATE':
+ addon_updater_ops.update_settings_ui(self, context)
diff --git a/uv_magic_uv/properites.py b/uv_magic_uv/properites.py
index d882063a..60ce26eb 100644
--- a/uv_magic_uv/properites.py
+++ b/uv_magic_uv/properites.py
@@ -20,746 +20,43 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
-import bpy
-from bpy.props import (
- FloatProperty,
- EnumProperty,
- BoolProperty,
- FloatVectorProperty,
- IntProperty
-)
-from mathutils import Vector
-from . import common
+from .utils.property_class_registry import PropertyClassRegistry
-def get_loaded_texture_name(_, __):
- items = [(key, key, "") for key in bpy.data.images.keys()]
- items.append(("None", "None", ""))
- return items
+__all__ = [
+ 'MUV_Properties',
+ 'init_props',
+ 'clear_props',
+]
# Properties used in this add-on.
+# pylint: disable=W0612
class MUV_Properties():
- cpuv = None
- cpuv_obj = None
- cpuv_selseq = None
- transuv = None
- uvbb = None
- texlock = None
- texproj = None
- texwrap = None
- mvuv = None
- uvinsp = None
- uvsculpt = 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.texlock = MUV_TexLockProps()
- self.texproj = MUV_TexProjProps()
- self.texwrap = MUV_TexWrapProps()
- self.mvuv = MUV_MVUVProps()
- self.uvinsp = MUV_UVInspProps()
- self.uvsculpt = MUV_UVSculptProps()
-
-
-class MUV_CPUVProps():
- src_uvs = []
- src_pin_uvs = []
- src_seams = []
-
-
-class MUV_CPUVSelSeqProps():
- src_uvs = []
- src_pin_uvs = []
- src_seams = []
-
-
-class MUV_TransUVProps():
- topology_copied = []
-
-
-class MUV_TexProjProps():
- running = False
-
-
-class MUV_UVBBProps():
- uv_info_ini = []
- ctrl_points_ini = []
- ctrl_points = []
- running = False
-
-
-class MUV_TexLockProps():
- verts_orig = None
- intr_verts_orig = None
- intr_running = False
-
-
-class MUV_TexWrapProps():
- ref_face_index = -1
- ref_obj = None
-
+ self.prefs = MUV_Prefs()
-class MUV_MVUVProps():
- running = False
-
-class MUV_UVInspProps():
- display_running = False
- overlapped_info = []
- flipped_info = []
-
-
-class MUV_UVSculptProps():
- running = False
+class MUV_Prefs():
+ expanded = {
+ "info_desc": False,
+ "info_loc": False,
+ "conf_uvsculpt": False,
+ "conf_uvinsp": False,
+ "conf_texproj": False,
+ "conf_uvbb": False
+ }
def init_props(scene):
scene.muv_props = MUV_Properties()
-
- # UV Sculpt
- scene.muv_uvsculpt_enabled = BoolProperty(
- name="UV Sculpt",
- description="UV Sculpt is enabled",
- default=False
- )
- scene.muv_uvsculpt_radius = IntProperty(
- name="Radius",
- description="Radius of the brush",
- min=1,
- max=500,
- default=30
- )
- scene.muv_uvsculpt_strength = FloatProperty(
- name="Strength",
- description="How powerful the effect of the brush when applied",
- min=0.0,
- max=1.0,
- default=0.03,
- )
- scene.muv_uvsculpt_tools = EnumProperty(
- name="Tools",
- description="Select Tools for the UV sculpt brushes",
- items=[
- ('GRAB', "Grab", "Grab UVs"),
- ('RELAX', "Relax", "Relax UVs"),
- ('PINCH', "Pinch", "Pinch UVs")
- ],
- default='GRAB'
- )
- scene.muv_uvsculpt_show_brush = BoolProperty(
- name="Show Brush",
- description="Show Brush",
- default=True
- )
- scene.muv_uvsculpt_pinch_invert = BoolProperty(
- name="Invert",
- description="Pinch UV to invert direction",
- default=False
- )
- scene.muv_uvsculpt_relax_method = EnumProperty(
- name="Method",
- description="Algorithm used for relaxation",
- items=[
- ('HC', "HC", "Use HC method for relaxation"),
- ('LAPLACIAN', "Laplacian", "Use laplacian method for relaxation")
- ],
- default='HC'
- )
-
- # Texture Wrap
- scene.muv_texwrap_enabled = BoolProperty(
- name="Texture Wrap",
- description="Texture Wrap is enabled",
- default=False
- )
- scene.muv_texwrap_set_and_refer = BoolProperty(
- name="Set and Refer",
- description="Refer and set UV",
- default=True
- )
- scene.muv_texwrap_selseq = BoolProperty(
- name="Selection Sequence",
- description="Set UV sequentially",
- default=False
- )
-
- # UV inspection
- scene.muv_seluv_enabled = BoolProperty(
- name="Select UV Enabled",
- description="Select UV is enabled",
- default=False
- )
- scene.muv_uvinsp_enabled = BoolProperty(
- name="UV Inspection Enabled",
- description="UV Inspection is enabled",
- default=False
- )
- scene.muv_uvinsp_show_overlapped = BoolProperty(
- name="Overlapped",
- description="Show overlapped UVs",
- default=False
- )
- scene.muv_uvinsp_show_flipped = BoolProperty(
- name="Flipped",
- description="Show flipped UVs",
- default=False
- )
- scene.muv_uvinsp_show_mode = EnumProperty(
- name="Mode",
- description="Show mode",
- items=[
- ('PART', "Part", "Show only overlapped/flipped part"),
- ('FACE', "Face", "Show overlapped/flipped face")
- ],
- default='PART'
- )
-
- # Align UV
- scene.muv_auv_enabled = BoolProperty(
- name="Align UV Enabled",
- description="Align UV is enabled",
- default=False
- )
- scene.muv_auv_transmission = BoolProperty(
- name="Transmission",
- description="Align linked UVs",
- default=False
- )
- scene.muv_auv_select = BoolProperty(
- name="Select",
- description="Select UVs which are aligned",
- default=False
- )
- scene.muv_auv_vertical = BoolProperty(
- name="Vert-Infl (Vertical)",
- description="Align vertical direction influenced "
- "by mesh vertex proportion",
- default=False
- )
- scene.muv_auv_horizontal = BoolProperty(
- name="Vert-Infl (Horizontal)",
- description="Align horizontal direction influenced "
- "by mesh vertex proportion",
- default=False
- )
- scene.muv_auv_location = EnumProperty(
- name="Location",
- description="Align location",
- items=[
- ('LEFT_TOP', "Left/Top", "Align to Left or Top"),
- ('MIDDLE', "Middle", "Align to middle"),
- ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom")
- ],
- default='MIDDLE'
- )
-
- # Smooth UV
- scene.muv_smuv_enabled = BoolProperty(
- name="Smooth UV Enabled",
- description="Smooth UV is enabled",
- default=False
- )
- scene.muv_smuv_transmission = BoolProperty(
- name="Transmission",
- description="Smooth linked UVs",
- default=False
- )
- scene.muv_smuv_mesh_infl = FloatProperty(
- name="Mesh Influence",
- description="Influence rate of mesh vertex",
- min=0.0,
- max=1.0,
- default=0.0
- )
- scene.muv_smuv_select = BoolProperty(
- name="Select",
- description="Select UVs which are smoothed",
- default=False
- )
-
- # UV Bounding Box
- scene.muv_uvbb_enabled = BoolProperty(
- name="UV Bounding Box Enabled",
- description="UV Bounding Box is enabled",
- default=False
- )
- scene.muv_uvbb_uniform_scaling = BoolProperty(
- name="Uniform Scaling",
- description="Enable Uniform Scaling",
- default=False
- )
- scene.muv_uvbb_boundary = EnumProperty(
- name="Boundary",
- description="Boundary",
- default='UV_SEL',
- items=[
- ('UV', "UV", "Boundary is decided by UV"),
- ('UV_SEL', "UV (Selected)", "Boundary is decided by Selected UV")
- ]
- )
-
- # Pack UV
- scene.muv_packuv_enabled = BoolProperty(
- name="Pack UV Enabled",
- description="Pack UV is enabled",
- default=False
- )
- scene.muv_packuv_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
- )
- scene.muv_packuv_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
- )
-
- # Move UV
- scene.muv_mvuv_enabled = BoolProperty(
- name="Move UV Enabled",
- description="Move UV is enabled",
- default=False
- )
-
- # UVW
- scene.muv_uvw_enabled = BoolProperty(
- name="UVW Enabled",
- description="UVW is enabled",
- default=False
- )
- scene.muv_uvw_assign_uvmap = BoolProperty(
- name="Assign UVMap",
- description="Assign UVMap when no UVmaps are available",
- default=True
- )
-
- # Texture Projection
- scene.muv_texproj_enabled = BoolProperty(
- name="Texture Projection Enabled",
- description="Texture Projection is enabled",
- 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
- )
- scene.muv_texproj_assign_uvmap = BoolProperty(
- name="Assign UVMap",
- description="Assign UVMap when no UVmaps are available",
- default=True
- )
-
- # Texture Lock
- scene.muv_texlock_enabled = BoolProperty(
- name="Texture Lock Enabled",
- description="Texture Lock is enabled",
- default=False
- )
- scene.muv_texlock_connect = BoolProperty(
- name="Connect UV",
- default=True
- )
-
- # World Scale UV
- scene.muv_wsuv_enabled = BoolProperty(
- name="World Scale UV Enabled",
- description="World Scale UV is enabled",
- default=False
- )
- scene.muv_wsuv_src_mesh_area = FloatProperty(
- name="Mesh Area",
- description="Source Mesh Area",
- default=0.0,
- min=0.0
- )
- scene.muv_wsuv_src_uv_area = FloatProperty(
- name="UV Area",
- description="Source UV Area",
- default=0.0,
- min=0.0
- )
- scene.muv_wsuv_src_density = FloatProperty(
- name="Density",
- description="Source Texel Density",
- default=0.0,
- min=0.0
- )
- scene.muv_wsuv_tgt_density = FloatProperty(
- name="Density",
- description="Target Texel Density",
- default=0.0,
- min=0.0
- )
- scene.muv_wsuv_mode = EnumProperty(
- name="Mode",
- description="Density calculation mode",
- items=[
- ('PROPORTIONAL', 'Proportional', 'Scale proportionally by mesh'),
- ('SCALING', 'Scaling', 'Specify scale factor'),
- ('USER', 'User', 'Specify density'),
- ('CONSTANT', 'Constant', 'Constant density')
- ],
- default='CONSTANT'
- )
- scene.muv_wsuv_scaling_factor = FloatProperty(
- name="Scaling Factor",
- default=1.0,
- max=1000.0,
- min=0.00001
- )
- scene.muv_wsuv_origin = EnumProperty(
- name="Origin",
- description="Aspect Origin",
- items=[
- ('CENTER', 'Center', 'Center'),
- ('LEFT_TOP', 'Left Top', 'Left Bottom'),
- ('LEFT_CENTER', 'Left Center', 'Left Center'),
- ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'),
- ('CENTER_TOP', 'Center Top', 'Center Top'),
- ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'),
- ('RIGHT_TOP', 'Right Top', 'Right Top'),
- ('RIGHT_CENTER', 'Right Center', 'Right Center'),
- ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom')
-
- ],
- default='CENTER'
- )
-
- # Unwrap Constraint
- scene.muv_unwrapconst_enabled = BoolProperty(
- name="Unwrap Constraint Enabled",
- description="Unwrap Constraint is enabled",
- default=False
- )
- scene.muv_unwrapconst_u_const = BoolProperty(
- name="U-Constraint",
- description="Keep UV U-axis coordinate",
- default=False
- )
- scene.muv_unwrapconst_v_const = BoolProperty(
- name="V-Constraint",
- description="Keep UV V-axis coordinate",
- default=False
- )
-
- # Preserve UV Aspect
- scene.muv_preserve_uv_enabled = BoolProperty(
- name="Preserve UV Aspect Enabled",
- description="Preserve UV Aspect is enabled",
- default=False
- )
- scene.muv_preserve_uv_tex_image = EnumProperty(
- name="Image",
- description="Texture Image",
- items=get_loaded_texture_name
- )
- scene.muv_preserve_uv_origin = EnumProperty(
- name="Origin",
- description="Aspect Origin",
- items=[
- ('CENTER', 'Center', 'Center'),
- ('LEFT_TOP', 'Left Top', 'Left Bottom'),
- ('LEFT_CENTER', 'Left Center', 'Left Center'),
- ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'),
- ('CENTER_TOP', 'Center Top', 'Center Top'),
- ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'),
- ('RIGHT_TOP', 'Right Top', 'Right Top'),
- ('RIGHT_CENTER', 'Right Center', 'Right Center'),
- ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom')
-
- ],
- default="CENTER"
- )
-
- # Flip/Rotate UV
- scene.muv_fliprot_enabled = BoolProperty(
- name="Flip/Rotate UV Enabled",
- description="Flip/Rotate UV is enabled",
- default=False
- )
- scene.muv_fliprot_seams = BoolProperty(
- name="Seams",
- description="Seams",
- default=True
- )
-
- # Mirror UV
- scene.muv_mirroruv_enabled = BoolProperty(
- name="Mirror UV Enabled",
- description="Mirror UV is enabled",
- default=False
- )
- scene.muv_mirroruv_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'
- )
-
- # Copy/Paste UV
- scene.muv_cpuv_enabled = BoolProperty(
- name="Copy/Paste UV Enabled",
- description="Copy/Paste UV is enabled",
- default=False
- )
- scene.muv_cpuv_copy_seams = BoolProperty(
- name="Copy Seams",
- description="Copy Seams",
- default=True
- )
- scene.muv_cpuv_mode = EnumProperty(
- items=[
- ('DEFAULT', "Default", "Default Mode"),
- ('SEL_SEQ', "Selection Sequence", "Selection Sequence Mode")
- ],
- name="Copy/Paste UV Mode",
- description="Copy/Paste UV Mode",
- default='DEFAULT'
- )
- scene.muv_cpuv_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'
- )
-
- # Transfer UV
- scene.muv_transuv_enabled = BoolProperty(
- name="Transfer UV Enabled",
- description="Transfer UV is enabled",
- default=False
- )
- scene.muv_transuv_invert_normals = BoolProperty(
- name="Invert Normals",
- description="Invert Normals",
- default=False
- )
- scene.muv_transuv_copy_seams = BoolProperty(
- name="Copy Seams",
- description="Copy Seams",
- default=True
- )
-
- # Align UV Cursor
- def auvc_get_cursor_loc(self):
- area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
- 'IMAGE_EDITOR')
- bd_size = common.get_uvimg_editor_board_size(area)
- loc = space.cursor_location
- if bd_size[0] < 0.000001:
- cx = 0.0
- else:
- cx = loc[0] / bd_size[0]
- if bd_size[1] < 0.000001:
- cy = 0.0
- else:
- cy = loc[1] / bd_size[1]
- self['muv_auvc_cursor_loc'] = Vector((cx, cy))
- return self.get('muv_auvc_cursor_loc', (0.0, 0.0))
-
- def auvc_set_cursor_loc(self, value):
- self['muv_auvc_cursor_loc'] = value
- area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
- 'IMAGE_EDITOR')
- bd_size = common.get_uvimg_editor_board_size(area)
- cx = bd_size[0] * value[0]
- cy = bd_size[1] * value[1]
- space.cursor_location = Vector((cx, cy))
-
- scene.muv_auvc_enabled = BoolProperty(
- name="Align UV Cursor Enabled",
- description="Align UV Cursor is enabled",
- default=False
- )
- scene.muv_auvc_cursor_loc = FloatVectorProperty(
- name="UV Cursor Location",
- size=2,
- precision=4,
- soft_min=-1.0,
- soft_max=1.0,
- step=1,
- default=(0.000, 0.000),
- get=auvc_get_cursor_loc,
- set=auvc_set_cursor_loc
- )
- scene.muv_auvc_align_menu = EnumProperty(
- name="Align Method",
- description="Align Method",
- default='TEXTURE',
- items=[
- ('TEXTURE', "Texture", "Align to texture"),
- ('UV', "UV", "Align to UV"),
- ('UV_SEL', "UV (Selected)", "Align to Selected UV")
- ]
- )
-
- # UV Cursor Location
- scene.muv_uvcloc_enabled = BoolProperty(
- name="UV Cursor Location Enabled",
- description="UV Cursor Location is enabled",
- default=False
- )
+ PropertyClassRegistry.init_props(scene)
def clear_props(scene):
+ PropertyClassRegistry.del_props(scene)
del scene.muv_props
-
- # UV Sculpt
- del scene.muv_uvsculpt_enabled
- del scene.muv_uvsculpt_radius
- del scene.muv_uvsculpt_strength
- del scene.muv_uvsculpt_tools
- del scene.muv_uvsculpt_show_brush
- del scene.muv_uvsculpt_pinch_invert
- del scene.muv_uvsculpt_relax_method
-
- # Texture Wrap
- del scene.muv_texwrap_enabled
- del scene.muv_texwrap_set_and_refer
- del scene.muv_texwrap_selseq
-
- # UV Inspection
- del scene.muv_seluv_enabled
- del scene.muv_uvinsp_enabled
- del scene.muv_uvinsp_show_overlapped
- del scene.muv_uvinsp_show_flipped
- del scene.muv_uvinsp_show_mode
-
- # Align UV
- del scene.muv_auv_enabled
- del scene.muv_auv_transmission
- del scene.muv_auv_select
- del scene.muv_auv_vertical
- del scene.muv_auv_horizontal
- del scene.muv_auv_location
-
- # Smooth UV
- del scene.muv_smuv_enabled
- del scene.muv_smuv_transmission
- del scene.muv_smuv_mesh_infl
- del scene.muv_smuv_select
-
- # UV Bounding Box
- del scene.muv_uvbb_enabled
- del scene.muv_uvbb_uniform_scaling
- del scene.muv_uvbb_boundary
-
- # Pack UV
- del scene.muv_packuv_enabled
- del scene.muv_packuv_allowable_center_deviation
- del scene.muv_packuv_allowable_size_deviation
-
- # Move UV
- del scene.muv_mvuv_enabled
-
- # UVW
- del scene.muv_uvw_enabled
- del scene.muv_uvw_assign_uvmap
-
- # Texture Projection
- del scene.muv_texproj_enabled
- 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
- del scene.muv_texproj_assign_uvmap
-
- # Texture Lock
- del scene.muv_texlock_enabled
- del scene.muv_texlock_connect
-
- # World Scale UV
- del scene.muv_wsuv_enabled
- del scene.muv_wsuv_src_mesh_area
- del scene.muv_wsuv_src_uv_area
- del scene.muv_wsuv_src_density
- del scene.muv_wsuv_tgt_density
- del scene.muv_wsuv_mode
- del scene.muv_wsuv_scaling_factor
- del scene.muv_wsuv_origin
-
- # Unwrap Constraint
- del scene.muv_unwrapconst_enabled
- del scene.muv_unwrapconst_u_const
- del scene.muv_unwrapconst_v_const
-
- # Preserve UV Aspect
- del scene.muv_preserve_uv_enabled
- del scene.muv_preserve_uv_tex_image
- del scene.muv_preserve_uv_origin
-
- # Flip/Rotate UV
- del scene.muv_fliprot_enabled
- del scene.muv_fliprot_seams
-
- # Mirror UV
- del scene.muv_mirroruv_enabled
- del scene.muv_mirroruv_axis
-
- # Copy/Paste UV
- del scene.muv_cpuv_enabled
- del scene.muv_cpuv_copy_seams
- del scene.muv_cpuv_mode
- del scene.muv_cpuv_strategy
-
- # Transfer UV
- del scene.muv_transuv_enabled
- del scene.muv_transuv_invert_normals
- del scene.muv_transuv_copy_seams
-
- # Align UV Cursor
- del scene.muv_auvc_enabled
- del scene.muv_auvc_cursor_loc
- del scene.muv_auvc_align_menu
-
- # UV Cursor Location
- del scene.muv_uvcloc_enabled
diff --git a/uv_magic_uv/ui/IMAGE_MT_uvs.py b/uv_magic_uv/ui/IMAGE_MT_uvs.py
new file mode 100644
index 00000000..e7dda379
--- /dev/null
+++ b/uv_magic_uv/ui/IMAGE_MT_uvs.py
@@ -0,0 +1,56 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import (
+ copy_paste_uv_uvedit,
+)
+from ..utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_MT_CopyPasteUV_UVEdit',
+]
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_UVEdit(bpy.types.Menu):
+ """
+ Menu class: Master menu of Copy/Paste UV coordinate on UV/ImageEditor
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_uvedit_menu"
+ bl_label = "Copy/Paste UV"
+ bl_description = "Copy and Paste UV coordinate among object"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.operator(
+ copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname,
+ text="Copy")
+ layout.operator(
+ copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname,
+ text="Paste")
diff --git a/uv_magic_uv/ui/VIEW3D_MT_object.py b/uv_magic_uv/ui/VIEW3D_MT_object.py
new file mode 100644
index 00000000..318cd82c
--- /dev/null
+++ b/uv_magic_uv/ui/VIEW3D_MT_object.py
@@ -0,0 +1,54 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from ..op import copy_paste_uv_object
+from ..utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_MT_CopyPasteUV_Object',
+]
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_Object(bpy.types.Menu):
+ """
+ Menu class: Master menu of Copy/Paste UV coordinate among object
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_object_menu"
+ bl_label = "Copy/Paste UV"
+ bl_description = "Copy and Paste UV coordinate among object"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.menu(
+ copy_paste_uv_object.MUV_MT_CopyPasteUVObject_CopyUV.bl_idname,
+ text="Copy")
+ layout.menu(
+ copy_paste_uv_object.MUV_MT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="Paste")
diff --git a/uv_magic_uv/ui/VIEW3D_MT_uv_map.py b/uv_magic_uv/ui/VIEW3D_MT_uv_map.py
new file mode 100644
index 00000000..c5698504
--- /dev/null
+++ b/uv_magic_uv/ui/VIEW3D_MT_uv_map.py
@@ -0,0 +1,111 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy.utils
+
+from ..op import (
+ copy_paste_uv,
+ transfer_uv,
+ uvw,
+)
+from ..utils.bl_class_registry import BlClassRegistry
+
+__all__ = [
+ 'MUV_MT_CopyPasteUV',
+ 'MUV_MT_TransferUV',
+ 'MUV_MT_UVW',
+]
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of Copy/Paste UV coordinate
+ """
+
+ bl_idname = "uv.muv_copy_paste_uv_menu"
+ bl_label = "Copy/Paste UV"
+ bl_description = "Copy and Paste UV coordinate"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.label(text="Default")
+ layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_CopyUV.bl_idname,
+ text="Copy")
+ layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_PasteUV.bl_idname,
+ text="Paste")
+
+ layout.separator()
+
+ layout.label(text="Selection Sequence")
+ layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="Copy")
+ layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="Paste")
+
+
+@BlClassRegistry()
+class MUV_MT_TransferUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of Transfer UV coordinate
+ """
+
+ bl_idname = "uv.muv_transfer_uv_menu"
+ bl_label = "Transfer UV"
+ bl_description = "Transfer UV coordinate"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.operator(transfer_uv.MUV_OT_TransferUV_CopyUV.bl_idname,
+ text="Copy")
+ ops = layout.operator(transfer_uv.MUV_OT_TransferUV_PasteUV.bl_idname,
+ text="Paste")
+ ops.invert_normals = sc.muv_transfer_uv_invert_normals
+ ops.copy_seams = sc.muv_transfer_uv_copy_seams
+
+
+@BlClassRegistry()
+class MUV_MT_UVW(bpy.types.Menu):
+ """
+ Menu class: Master menu of UVW
+ """
+
+ bl_idname = "uv.muv_uvw_menu"
+ bl_label = "UVW"
+ bl_description = ""
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ ops = layout.operator(uvw.MUV_OT_UVW_BoxMap.bl_idname, text="Box")
+ ops.assign_uvmap = sc.muv_uvw_assign_uvmap
+
+ ops = layout.operator(uvw.MUV_OT_UVW_BestPlanerMap.bl_idname,
+ text="Best Planner")
+ ops.assign_uvmap = sc.muv_uvw_assign_uvmap
diff --git a/uv_magic_uv/ui/__init__.py b/uv_magic_uv/ui/__init__.py
index ad56aeb3..5f7e0c5e 100644
--- a/uv_magic_uv/ui/__init__.py
+++ b/uv_magic_uv/ui/__init__.py
@@ -20,25 +20,27 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
if "bpy" in locals():
import importlib
- importlib.reload(view3d_copy_paste_uv_objectmode)
importlib.reload(view3d_copy_paste_uv_editmode)
+ importlib.reload(view3d_copy_paste_uv_objectmode)
importlib.reload(view3d_uv_manipulation)
importlib.reload(view3d_uv_mapping)
importlib.reload(uvedit_copy_paste_uv)
- importlib.reload(uvedit_uv_manipulation)
- importlib.reload(uvedit_editor_enhance)
+ importlib.reload(VIEW3D_MT_object)
+ importlib.reload(VIEW3D_MT_uv_map)
+ importlib.reload(IMAGE_MT_uvs)
else:
- from . import view3d_copy_paste_uv_objectmode
from . import view3d_copy_paste_uv_editmode
+ from . import view3d_copy_paste_uv_objectmode
from . import view3d_uv_manipulation
from . import view3d_uv_mapping
from . import uvedit_copy_paste_uv
- from . import uvedit_uv_manipulation
- from . import uvedit_editor_enhance
+ from . import VIEW3D_MT_object
+ from . import VIEW3D_MT_uv_map
+ from . import IMAGE_MT_uvs
import bpy
diff --git a/uv_magic_uv/ui/uvedit_copy_paste_uv.py b/uv_magic_uv/ui/uvedit_copy_paste_uv.py
index d87dbef3..e21a5abd 100644
--- a/uv_magic_uv/ui/uvedit_copy_paste_uv.py
+++ b/uv_magic_uv/ui/uvedit_copy_paste_uv.py
@@ -20,21 +20,27 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
from ..op import copy_paste_uv_uvedit
+from ..utils.bl_class_registry import BlClassRegistry
+__all__ = [
+ 'MUV_PT_UVEdit_CopyPasteUV',
+]
-class IMAGE_PT_MUV_CPUV(bpy.types.Panel):
+
+@BlClassRegistry()
+class MUV_PT_UVEdit_CopyPasteUV(bpy.types.Panel):
"""
Panel class: Copy/Paste UV on Property Panel on UV/ImageEditor
"""
bl_space_type = 'IMAGE_EDITOR'
- bl_region_type = 'TOOLS'
+ bl_region_type = 'UI'
bl_label = "Copy/Paste UV"
bl_category = "Magic UV"
bl_context = 'mesh_edit'
@@ -42,13 +48,15 @@ class IMAGE_PT_MUV_CPUV(bpy.types.Panel):
def draw_header(self, _):
layout = self.layout
- layout.label(text="", icon='IMAGE_COL')
+ layout.label(text="", icon='IMAGE')
def draw(self, _):
layout = self.layout
row = layout.row(align=True)
- row.operator(copy_paste_uv_uvedit.MUV_CPUVIECopyUV.bl_idname,
- text="Copy")
- row.operator(copy_paste_uv_uvedit.MUV_CPUVIEPasteUV.bl_idname,
- text="Paste")
+ row.operator(
+ copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname,
+ text="Copy")
+ row.operator(
+ copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname,
+ text="Paste")
diff --git a/uv_magic_uv/ui/uvedit_editor_enhance.py b/uv_magic_uv/ui/uvedit_editor_enhance.py
deleted file mode 100644
index 88a2492c..00000000
--- a/uv_magic_uv/ui/uvedit_editor_enhance.py
+++ /dev/null
@@ -1,136 +0,0 @@
-# <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__ = "5.1"
-__date__ = "24 Feb 2018"
-
-import bpy
-
-from ..op import align_uv_cursor
-from ..op import uv_bounding_box
-from ..op import uv_inspection
-
-
-class IMAGE_PT_MUV_EE(bpy.types.Panel):
- """
- Panel class: UV/Image Editor Enhancement
- """
-
- bl_space_type = 'IMAGE_EDITOR'
- bl_region_type = 'TOOLS'
- bl_label = "Editor Enhancement"
- bl_category = "Magic UV"
- bl_context = 'mesh_edit'
- bl_options = {'DEFAULT_CLOSED'}
-
- def draw_header(self, _):
- layout = self.layout
- layout.label(text="", icon='IMAGE_COL')
-
- def draw(self, context):
- layout = self.layout
- sc = context.scene
- props = sc.muv_props
-
- box = layout.box()
- box.prop(sc, "muv_auvc_enabled", text="Align UV Cursor")
- if sc.muv_auvc_enabled:
- box.prop(sc, "muv_auvc_align_menu", expand=True)
-
- col = box.column(align=True)
-
- row = col.row(align=True)
- ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname,
- text="Left Top")
- ops.position = 'LEFT_TOP'
- ops.base = sc.muv_auvc_align_menu
- ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname,
- text="Middle Top")
- ops.position = 'MIDDLE_TOP'
- ops.base = sc.muv_auvc_align_menu
- ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname,
- text="Right Top")
- ops.position = 'RIGHT_TOP'
- ops.base = sc.muv_auvc_align_menu
-
- row = col.row(align=True)
- ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname,
- text="Left Middle")
- ops.position = 'LEFT_MIDDLE'
- ops.base = sc.muv_auvc_align_menu
- ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname,
- text="Center")
- ops.position = 'CENTER'
- ops.base = sc.muv_auvc_align_menu
- ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname,
- text="Right Middle")
- ops.position = 'RIGHT_MIDDLE'
- ops.base = sc.muv_auvc_align_menu
-
- row = col.row(align=True)
- ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname,
- text="Left Bottom")
- ops.position = 'LEFT_BOTTOM'
- ops.base = sc.muv_auvc_align_menu
- ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname,
- text="Middle Bottom")
- ops.position = 'MIDDLE_BOTTOM'
- ops.base = sc.muv_auvc_align_menu
- ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname,
- text="Right Bottom")
- ops.position = 'RIGHT_BOTTOM'
- ops.base = sc.muv_auvc_align_menu
-
- box = layout.box()
- box.prop(sc, "muv_uvcloc_enabled", text="UV Cursor Location")
- if sc.muv_uvcloc_enabled:
- box.prop(sc, "muv_auvc_cursor_loc", text="")
-
- box = layout.box()
- box.prop(sc, "muv_uvbb_enabled", text="UV Bounding Box")
- if sc.muv_uvbb_enabled:
- if props.uvbb.running is False:
- box.operator(uv_bounding_box.MUV_UVBBUpdater.bl_idname,
- text="Display", icon='PLAY')
- else:
- box.operator(uv_bounding_box.MUV_UVBBUpdater.bl_idname,
- text="Hide", icon='PAUSE')
- box.prop(sc, "muv_uvbb_uniform_scaling", text="Uniform Scaling")
- box.prop(sc, "muv_uvbb_boundary", text="Boundary")
-
- box = layout.box()
- box.prop(sc, "muv_uvinsp_enabled", text="UV Inspection")
- if sc.muv_uvinsp_enabled:
- row = box.row()
- if not sc.muv_props.uvinsp.display_running:
- row.operator(uv_inspection.MUV_UVInspDisplay.bl_idname,
- text="Display", icon='PLAY')
- else:
- row.operator(uv_inspection.MUV_UVInspDisplay.bl_idname,
- text="Hide", icon='PAUSE')
- row.operator(uv_inspection.MUV_UVInspUpdate.bl_idname,
- text="Update")
- row = box.row()
- row.prop(sc, "muv_uvinsp_show_overlapped")
- row.prop(sc, "muv_uvinsp_show_flipped")
- row = box.row()
- row.prop(sc, "muv_uvinsp_show_mode")
diff --git a/uv_magic_uv/ui/uvedit_uv_manipulation.py b/uv_magic_uv/ui/uvedit_uv_manipulation.py
deleted file mode 100644
index f391c4cb..00000000
--- a/uv_magic_uv/ui/uvedit_uv_manipulation.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# <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__ = "5.1"
-__date__ = "24 Feb 2018"
-
-import bpy
-
-from ..op import uv_inspection
-from ..op import align_uv
-from ..op import smooth_uv
-from ..op import pack_uv
-
-
-class IMAGE_PT_MUV_UVManip(bpy.types.Panel):
- """
- Panel class: UV Manipulation on Property Panel on UV/ImageEditor
- """
-
- bl_space_type = 'IMAGE_EDITOR'
- bl_region_type = 'TOOLS'
- bl_label = "UV Manipulation"
- bl_category = "Magic UV"
- bl_context = 'mesh_edit'
- bl_options = {'DEFAULT_CLOSED'}
-
- def draw_header(self, _):
- layout = self.layout
- layout.label(text="", icon='IMAGE_COL')
-
- def draw(self, context):
- sc = context.scene
- layout = self.layout
-
- box = layout.box()
- box.prop(sc, "muv_auv_enabled", text="Align UV")
- if sc.muv_auv_enabled:
- col = box.column()
- row = col.row(align=True)
- ops = row.operator(align_uv.MUV_AUVCircle.bl_idname, text="Circle")
- ops.transmission = sc.muv_auv_transmission
- ops.select = sc.muv_auv_select
- ops = row.operator(align_uv.MUV_AUVStraighten.bl_idname,
- text="Straighten")
- ops.transmission = sc.muv_auv_transmission
- ops.select = sc.muv_auv_select
- ops.vertical = sc.muv_auv_vertical
- ops.horizontal = sc.muv_auv_horizontal
- row = col.row()
- ops = row.operator(align_uv.MUV_AUVAxis.bl_idname, text="XY-axis")
- ops.transmission = sc.muv_auv_transmission
- ops.select = sc.muv_auv_select
- ops.vertical = sc.muv_auv_vertical
- ops.horizontal = sc.muv_auv_horizontal
- ops.location = sc.muv_auv_location
- row.prop(sc, "muv_auv_location", text="")
-
- col = box.column(align=True)
- row = col.row(align=True)
- row.prop(sc, "muv_auv_transmission", text="Transmission")
- row.prop(sc, "muv_auv_select", text="Select")
- row = col.row(align=True)
- row.prop(sc, "muv_auv_vertical", text="Vertical")
- row.prop(sc, "muv_auv_horizontal", text="Horizontal")
-
- box = layout.box()
- box.prop(sc, "muv_smuv_enabled", text="Smooth UV")
- if sc.muv_smuv_enabled:
- ops = box.operator(smooth_uv.MUV_AUVSmooth.bl_idname,
- text="Smooth")
- ops.transmission = sc.muv_smuv_transmission
- ops.select = sc.muv_smuv_select
- ops.mesh_infl = sc.muv_smuv_mesh_infl
- col = box.column(align=True)
- row = col.row(align=True)
- row.prop(sc, "muv_smuv_transmission", text="Transmission")
- row.prop(sc, "muv_smuv_select", text="Select")
- col.prop(sc, "muv_smuv_mesh_infl", text="Mesh Influence")
-
- box = layout.box()
- box.prop(sc, "muv_seluv_enabled", text="Select UV")
- if sc.muv_seluv_enabled:
- row = box.row(align=True)
- row.operator(uv_inspection.MUV_UVInspSelectOverlapped.bl_idname)
- row.operator(uv_inspection.MUV_UVInspSelectFlipped.bl_idname)
-
- box = layout.box()
- box.prop(sc, "muv_packuv_enabled", text="Pack UV (Extension)")
- if sc.muv_packuv_enabled:
- ops = box.operator(pack_uv.MUV_PackUV.bl_idname, text="Pack UV")
- ops.allowable_center_deviation = \
- sc.muv_packuv_allowable_center_deviation
- ops.allowable_size_deviation = \
- sc.muv_packuv_allowable_size_deviation
- box.label("Allowable Center Deviation:")
- box.prop(sc, "muv_packuv_allowable_center_deviation", text="")
- box.label("Allowable Size Deviation:")
- box.prop(sc, "muv_packuv_allowable_size_deviation", text="")
diff --git a/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py b/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py
index a22adf03..14fba24a 100644
--- a/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py
+++ b/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py
@@ -20,22 +20,30 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
-from ..op import copy_paste_uv
-from ..op import transfer_uv
+from ..op import (
+ copy_paste_uv,
+ transfer_uv,
+)
+from ..utils.bl_class_registry import BlClassRegistry
+__all__ = [
+ 'MUV_PT_CopyPasteUVEditMode',
+]
-class OBJECT_PT_MUV_CPUV(bpy.types.Panel):
+
+@BlClassRegistry()
+class MUV_PT_CopyPasteUVEditMode(bpy.types.Panel):
"""
Panel class: Copy/Paste UV on Property Panel on View3D
"""
bl_space_type = 'VIEW_3D'
- bl_region_type = 'TOOLS'
+ bl_region_type = 'UI'
bl_label = "Copy/Paste UV"
bl_category = "Magic UV"
bl_context = 'mesh_edit'
@@ -43,39 +51,43 @@ class OBJECT_PT_MUV_CPUV(bpy.types.Panel):
def draw_header(self, _):
layout = self.layout
- layout.label(text="", icon='IMAGE_COL')
+ layout.label(text="", icon='IMAGE')
def draw(self, context):
sc = context.scene
layout = self.layout
box = layout.box()
- box.prop(sc, "muv_cpuv_enabled", text="Copy/Paste UV")
- if sc.muv_cpuv_enabled:
+ box.prop(sc, "muv_copy_paste_uv_enabled", text="Copy/Paste UV")
+ if sc.muv_copy_paste_uv_enabled:
row = box.row(align=True)
- if sc.muv_cpuv_mode == 'DEFAULT':
- row.menu(copy_paste_uv.MUV_CPUVCopyUVMenu.bl_idname,
- text="Copy")
- row.menu(copy_paste_uv.MUV_CPUVPasteUVMenu.bl_idname,
- text="Paste")
- elif sc.muv_cpuv_mode == 'SEL_SEQ':
- row.menu(copy_paste_uv.MUV_CPUVSelSeqCopyUVMenu.bl_idname,
+ if sc.muv_copy_paste_uv_mode == 'DEFAULT':
+ row.menu(copy_paste_uv.MUV_MT_CopyPasteUV_CopyUV.bl_idname,
text="Copy")
- row.menu(copy_paste_uv.MUV_CPUVSelSeqPasteUVMenu.bl_idname,
+ row.menu(copy_paste_uv.MUV_MT_CopyPasteUV_PasteUV.bl_idname,
text="Paste")
- box.prop(sc, "muv_cpuv_mode", expand=True)
- box.prop(sc, "muv_cpuv_copy_seams", text="Seams")
- box.prop(sc, "muv_cpuv_strategy", text="Strategy")
+ elif sc.muv_copy_paste_uv_mode == 'SEL_SEQ':
+ row.menu(
+ copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="Copy")
+ row.menu(
+ copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="Paste")
+ box.prop(sc, "muv_copy_paste_uv_mode", expand=True)
+ box.prop(sc, "muv_copy_paste_uv_copy_seams", text="Seams")
+ box.prop(sc, "muv_copy_paste_uv_strategy", text="Strategy")
box = layout.box()
- box.prop(sc, "muv_transuv_enabled", text="Transfer UV")
- if sc.muv_transuv_enabled:
+ box.prop(sc, "muv_transfer_uv_enabled", text="Transfer UV")
+ if sc.muv_transfer_uv_enabled:
row = box.row(align=True)
- row.operator(transfer_uv.MUV_TransUVCopy.bl_idname, text="Copy")
- ops = row.operator(transfer_uv.MUV_TransUVPaste.bl_idname,
+ row.operator(transfer_uv.MUV_OT_TransferUV_CopyUV.bl_idname,
+ text="Copy")
+ ops = row.operator(transfer_uv.MUV_OT_TransferUV_PasteUV.bl_idname,
text="Paste")
- ops.invert_normals = sc.muv_transuv_invert_normals
- ops.copy_seams = sc.muv_transuv_copy_seams
+ ops.invert_normals = sc.muv_transfer_uv_invert_normals
+ ops.copy_seams = sc.muv_transfer_uv_copy_seams
row = box.row()
- row.prop(sc, "muv_transuv_invert_normals", text="Invert Normals")
- row.prop(sc, "muv_transuv_copy_seams", text="Seams")
+ row.prop(sc, "muv_transfer_uv_invert_normals",
+ text="Invert Normals")
+ row.prop(sc, "muv_transfer_uv_copy_seams", text="Seams")
diff --git a/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py b/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py
index f9e2bec0..6dd0d3b4 100644
--- a/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py
+++ b/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py
@@ -20,21 +20,27 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
from ..op import copy_paste_uv_object
+from ..utils.bl_class_registry import BlClassRegistry
+__all__ = [
+ 'MUV_PT_View3D_Object_CopyPasteUV',
+]
-class OBJECT_PT_MUV_CPUVObj(bpy.types.Panel):
+
+@BlClassRegistry()
+class MUV_PT_View3D_Object_CopyPasteUV(bpy.types.Panel):
"""
Panel class: Copy/Paste UV on Property Panel on View3D
"""
bl_space_type = 'VIEW_3D'
- bl_region_type = 'TOOLS'
+ bl_region_type = 'UI'
bl_label = "Copy/Paste UV"
bl_category = "Magic UV"
bl_context = 'objectmode'
@@ -42,15 +48,18 @@ class OBJECT_PT_MUV_CPUVObj(bpy.types.Panel):
def draw_header(self, _):
layout = self.layout
- layout.label(text="", icon='IMAGE_COL')
+ layout.label(text="", icon='IMAGE')
def draw(self, context):
sc = context.scene
layout = self.layout
row = layout.row(align=True)
- row.menu(copy_paste_uv_object.MUV_CPUVObjCopyUVMenu.bl_idname,
- text="Copy")
- row.menu(copy_paste_uv_object.MUV_CPUVObjPasteUVMenu.bl_idname,
- text="Paste")
- layout.prop(sc, "muv_cpuv_copy_seams", text="Copy Seams")
+ row.menu(
+ copy_paste_uv_object.MUV_MT_CopyPasteUVObject_CopyUV.bl_idname,
+ text="Copy")
+ row.menu(
+ copy_paste_uv_object.MUV_MT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="Paste")
+ layout.prop(sc, "muv_copy_paste_uv_object_copy_seams",
+ text="Seams")
diff --git a/uv_magic_uv/ui/view3d_uv_manipulation.py b/uv_magic_uv/ui/view3d_uv_manipulation.py
index 1e9b7d7e..365a0dc8 100644
--- a/uv_magic_uv/ui/view3d_uv_manipulation.py
+++ b/uv_magic_uv/ui/view3d_uv_manipulation.py
@@ -20,28 +20,31 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
-from ..op import flip_rotate_uv
-from ..op import mirror_uv
-from ..op import move_uv
-from ..op import preserve_uv_aspect
-from ..op import texture_lock
-from ..op import texture_wrap
-from ..op import uv_sculpt
-from ..op import world_scale_uv
+from ..op import (
+ flip_rotate_uv,
+ mirror_uv,
+ move_uv,
+)
+from ..utils.bl_class_registry import BlClassRegistry
+__all__ = [
+ 'MUV_PT_View3D_UVManipulation',
+]
-class OBJECT_PT_MUV_UVManip(bpy.types.Panel):
+
+@BlClassRegistry()
+class MUV_PT_View3D_UVManipulation(bpy.types.Panel):
"""
Panel class: UV Manipulation on Property Panel on View3D
"""
bl_space_type = 'VIEW_3D'
- bl_region_type = 'TOOLS'
+ bl_region_type = 'UI'
bl_label = "UV Manipulation"
bl_category = "Magic UV"
bl_context = 'mesh_edit'
@@ -49,132 +52,37 @@ class OBJECT_PT_MUV_UVManip(bpy.types.Panel):
def draw_header(self, _):
layout = self.layout
- layout.label(text="", icon='IMAGE_COL')
+ layout.label(text="", icon='IMAGE')
def draw(self, context):
sc = context.scene
- props = sc.muv_props
layout = self.layout
box = layout.box()
- box.prop(sc, "muv_fliprot_enabled", text="Flip/Rotate UV")
- if sc.muv_fliprot_enabled:
+ box.prop(sc, "muv_flip_rotate_uv_enabled", text="Flip/Rotate UV")
+ if sc.muv_flip_rotate_uv_enabled:
row = box.row()
- ops = row.operator(flip_rotate_uv.MUV_FlipRot.bl_idname,
+ ops = row.operator(flip_rotate_uv.MUV_OT_FlipRotate.bl_idname,
text="Flip/Rotate")
- ops.seams = sc.muv_fliprot_seams
- row.prop(sc, "muv_fliprot_seams", text="Seams")
+ ops.seams = sc.muv_flip_rotate_uv_seams
+ row.prop(sc, "muv_flip_rotate_uv_seams", text="Seams")
box = layout.box()
- box.prop(sc, "muv_mirroruv_enabled", text="Mirror UV")
- if sc.muv_mirroruv_enabled:
+ box.prop(sc, "muv_mirror_uv_enabled", text="Mirror UV")
+ if sc.muv_mirror_uv_enabled:
row = box.row()
- ops = row.operator(mirror_uv.MUV_MirrorUV.bl_idname, text="Mirror")
- ops.axis = sc.muv_mirroruv_axis
- row.prop(sc, "muv_mirroruv_axis", text="")
+ ops = row.operator(mirror_uv.MUV_OT_MirrorUV.bl_idname,
+ text="Mirror")
+ ops.axis = sc.muv_mirror_uv_axis
+ row.prop(sc, "muv_mirror_uv_axis", text="")
box = layout.box()
- box.prop(sc, "muv_mvuv_enabled", text="Move UV")
- if sc.muv_mvuv_enabled:
+ box.prop(sc, "muv_move_uv_enabled", text="Move UV")
+ if sc.muv_move_uv_enabled:
col = box.column()
- col.operator(move_uv.MUV_MVUV.bl_idname, icon='PLAY', text="Start")
- if props.mvuv.running:
- col.enabled = False
- else:
- col.enabled = True
-
- box = layout.box()
- box.prop(sc, "muv_wsuv_enabled", text="World Scale UV")
- if sc.muv_wsuv_enabled:
- row = box.row(align=True)
- row.operator(world_scale_uv.MUV_WSUVMeasure.bl_idname,
- text="Measure")
- ops = row.operator(world_scale_uv.MUV_WSUVApply.bl_idname,
- text="Apply")
- ops.origin = sc.muv_wsuv_origin
- box.label("Source:")
- sp = box.split(percentage=0.7)
- col = sp.column(align=True)
- col.prop(sc, "muv_wsuv_src_mesh_area", text="Mesh Area")
- col.prop(sc, "muv_wsuv_src_uv_area", text="UV Area")
- col.prop(sc, "muv_wsuv_src_density", text="Density")
- col.enabled = False
- sp = sp.split(percentage=1.0)
- col = sp.column(align=True)
- col.label("cm x cm")
- col.label("px x px")
- col.label("px/cm")
- col.enabled = False
- sp = box.split(percentage=0.3)
- sp.label("Mode:")
- sp = sp.split(percentage=1.0)
- col = sp.column()
- col.prop(sc, "muv_wsuv_mode", text="")
- if sc.muv_wsuv_mode == 'USER':
- col.prop(sc, "muv_wsuv_tgt_density", text="Density")
- if sc.muv_wsuv_mode == 'SCALING':
- col.prop(sc, "muv_wsuv_scaling_factor", text="Scaling Factor")
- box.prop(sc, "muv_wsuv_origin", text="Origin")
-
- box = layout.box()
- box.prop(sc, "muv_preserve_uv_enabled", text="Preserve UV Aspect")
- if sc.muv_preserve_uv_enabled:
- row = box.row()
- ops = row.operator(
- preserve_uv_aspect.MUV_PreserveUVAspect.bl_idname,
- text="Change Image")
- ops.dest_img_name = sc.muv_preserve_uv_tex_image
- ops.origin = sc.muv_preserve_uv_origin
- row.prop(sc, "muv_preserve_uv_tex_image", text="")
- box.prop(sc, "muv_preserve_uv_origin", text="Origin")
-
- box = layout.box()
- box.prop(sc, "muv_texlock_enabled", text="Texture Lock")
- if sc.muv_texlock_enabled:
- row = box.row(align=True)
- col = row.column(align=True)
- col.label("Normal Mode:")
- col = row.column(align=True)
- col.operator(texture_lock.MUV_TexLockStart.bl_idname, text="Lock")
- ops = col.operator(texture_lock.MUV_TexLockStop.bl_idname,
- text="Unlock")
- ops.connect = sc.muv_texlock_connect
- col.prop(sc, "muv_texlock_connect", text="Connect")
-
- row = box.row(align=True)
- row.label("Interactive Mode:")
- if not props.texlock.intr_running:
- row.operator(texture_lock.MUV_TexLockIntrStart.bl_idname,
- icon='PLAY', text="Start")
- else:
- row.operator(texture_lock.MUV_TexLockIntrStop.bl_idname,
- icon="PAUSE", text="Stop")
-
- box = layout.box()
- box.prop(sc, "muv_texwrap_enabled", text="Texture Wrap")
- if sc.muv_texwrap_enabled:
- row = box.row(align=True)
- row.operator(texture_wrap.MUV_TexWrapRefer.bl_idname, text="Refer")
- row.operator(texture_wrap.MUV_TexWrapSet.bl_idname, text="Set")
- box.prop(sc, "muv_texwrap_set_and_refer")
- box.prop(sc, "muv_texwrap_selseq")
-
- box = layout.box()
- box.prop(sc, "muv_uvsculpt_enabled", text="UV Sculpt")
- if sc.muv_uvsculpt_enabled:
- if not props.uvsculpt.running:
- box.operator(uv_sculpt.MUV_UVSculptOps.bl_idname,
- icon='PLAY', text="Start")
+ if not move_uv.MUV_OT_MoveUV.is_running(context):
+ col.operator(move_uv.MUV_OT_MoveUV.bl_idname, icon='PLAY',
+ text="Start")
else:
- box.operator(uv_sculpt.MUV_UVSculptOps.bl_idname,
- icon='PAUSE', text="Stop")
- col = box.column()
- col.label("Brush:")
- col.prop(sc, "muv_uvsculpt_radius")
- col.prop(sc, "muv_uvsculpt_strength")
- box.prop(sc, "muv_uvsculpt_tools")
- if sc.muv_uvsculpt_tools == 'PINCH':
- box.prop(sc, "muv_uvsculpt_pinch_invert")
- elif sc.muv_uvsculpt_tools == 'RELAX':
- box.prop(sc, "muv_uvsculpt_relax_method")
- box.prop(sc, "muv_uvsculpt_show_brush")
+ col.operator(move_uv.MUV_OT_MoveUV.bl_idname, icon='PAUSE',
+ text="Stop")
diff --git a/uv_magic_uv/ui/view3d_uv_mapping.py b/uv_magic_uv/ui/view3d_uv_mapping.py
index 2dc241c0..c596008e 100644
--- a/uv_magic_uv/ui/view3d_uv_mapping.py
+++ b/uv_magic_uv/ui/view3d_uv_mapping.py
@@ -20,23 +20,29 @@
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
-__version__ = "5.1"
-__date__ = "24 Feb 2018"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
import bpy
-from ..op import texture_projection
-from ..op import unwrap_constraint
-from ..op import uvw
+from ..op import (
+ uvw,
+)
+from ..utils.bl_class_registry import BlClassRegistry
+__all__ = [
+ 'MUV_PT_View3D_UVMapping',
+]
-class OBJECT_PT_MUV_UVMapping(bpy.types.Panel):
+
+@BlClassRegistry()
+class MUV_PT_View3D_UVMapping(bpy.types.Panel):
"""
Panel class: UV Mapping on Property Panel on View3D
"""
bl_space_type = 'VIEW_3D'
- bl_region_type = 'TOOLS'
+ bl_region_type = 'UI'
bl_label = "UV Mapping"
bl_category = "Magic UV"
bl_context = 'mesh_edit'
@@ -44,56 +50,19 @@ class OBJECT_PT_MUV_UVMapping(bpy.types.Panel):
def draw_header(self, _):
layout = self.layout
- layout.label(text="", icon='IMAGE_COL')
+ layout.label(text="", icon='IMAGE')
def draw(self, context):
sc = context.scene
- props = sc.muv_props
layout = self.layout
box = layout.box()
- box.prop(sc, "muv_unwrapconst_enabled", text="Unwrap Constraint")
- if sc.muv_unwrapconst_enabled:
- ops = box.operator(
- unwrap_constraint.MUV_UnwrapConstraint.bl_idname,
- text="Unwrap")
- ops.u_const = sc.muv_unwrapconst_u_const
- ops.v_const = sc.muv_unwrapconst_v_const
- row = box.row(align=True)
- row.prop(sc, "muv_unwrapconst_u_const", text="U-Constraint")
- row.prop(sc, "muv_unwrapconst_v_const", text="V-Constraint")
-
- box = layout.box()
- box.prop(sc, "muv_texproj_enabled", text="Texture Projection")
- if sc.muv_texproj_enabled:
- row = box.row()
- if not props.texproj.running:
- row.operator(texture_projection.MUV_TexProjStart.bl_idname,
- text="Start", icon='PLAY')
- else:
- row.operator(texture_projection.MUV_TexProjStop.bl_idname,
- text="Stop", icon='PAUSE')
- row.prop(sc, "muv_texproj_tex_image", text="")
- box.prop(sc, "muv_texproj_tex_transparency", text="Transparency")
- col = box.column(align=True)
- row = col.row()
- row.prop(sc, "muv_texproj_adjust_window", text="Adjust Window")
- if not sc.muv_texproj_adjust_window:
- row.prop(sc, "muv_texproj_tex_magnitude", text="Magnitude")
- col.prop(sc, "muv_texproj_apply_tex_aspect",
- text="Texture Aspect Ratio")
- col.prop(sc, "muv_texproj_assign_uvmap", text="Assign UVMap")
- if props.texproj.running:
- box.operator(texture_projection.MUV_TexProjProject.bl_idname,
- text="Project")
-
- box = layout.box()
box.prop(sc, "muv_uvw_enabled", text="UVW")
if sc.muv_uvw_enabled:
row = box.row(align=True)
- ops = row.operator(uvw.MUV_UVWBoxMap.bl_idname, text="Box")
+ ops = row.operator(uvw.MUV_OT_UVW_BoxMap.bl_idname, text="Box")
ops.assign_uvmap = sc.muv_uvw_assign_uvmap
- ops = row.operator(uvw.MUV_UVWBestPlanerMap.bl_idname,
+ ops = row.operator(uvw.MUV_OT_UVW_BestPlanerMap.bl_idname,
text="Best Planner")
ops.assign_uvmap = sc.muv_uvw_assign_uvmap
box.prop(sc, "muv_uvw_assign_uvmap", text="Assign UVMap")
diff --git a/uv_magic_uv/utils/__init__.py b/uv_magic_uv/utils/__init__.py
new file mode 100644
index 00000000..4ce9d907
--- /dev/null
+++ b/uv_magic_uv/utils/__init__.py
@@ -0,0 +1,34 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(bl_class_registry)
+ importlib.reload(property_class_registry)
+else:
+ from . import bl_class_registry
+ from . import property_class_registry
+
+import bpy
diff --git a/uv_magic_uv/utils/bl_class_registry.py b/uv_magic_uv/utils/bl_class_registry.py
new file mode 100644
index 00000000..d1730615
--- /dev/null
+++ b/uv_magic_uv/utils/bl_class_registry.py
@@ -0,0 +1,84 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+
+from .. import common
+
+__all__ = [
+ 'BlClassRegistry',
+]
+
+
+class BlClassRegistry:
+ class_list = []
+
+ def __init__(self, *_, **kwargs):
+ self.legacy = kwargs.get('legacy', False)
+
+ def __call__(self, cls):
+ if hasattr(cls, "bl_idname"):
+ BlClassRegistry.add_class(cls.bl_idname, cls, self.legacy)
+ else:
+ bl_idname = "{}{}{}{}".format(cls.bl_space_type,
+ cls.bl_region_type,
+ cls.bl_context, cls.bl_label)
+ BlClassRegistry.add_class(bl_idname, cls, self.legacy)
+ return cls
+
+ @classmethod
+ def add_class(cls, bl_idname, op_class, legacy):
+ for class_ in cls.class_list:
+ if (class_["bl_idname"] == bl_idname) and \
+ (class_["legacy"] == legacy):
+ raise RuntimeError("{} is already registered"
+ .format(bl_idname))
+
+ new_op = {
+ "bl_idname": bl_idname,
+ "class": op_class,
+ "legacy": legacy,
+ }
+ cls.class_list.append(new_op)
+ common.debug_print("{} is registered.".format(bl_idname))
+
+ @classmethod
+ def register(cls):
+ for class_ in cls.class_list:
+ bpy.utils.register_class(class_["class"])
+ common.debug_print("{} is registered to Blender."
+ .format(class_["bl_idname"]))
+
+ @classmethod
+ def unregister(cls):
+ for class_ in cls.class_list:
+ bpy.utils.unregister_class(class_["class"])
+ common.debug_print("{} is unregistered from Blender."
+ .format(class_["bl_idname"]))
+
+ @classmethod
+ def cleanup(cls):
+ cls.class_list = []
+ common.debug_print("Cleanup registry.")
diff --git a/uv_magic_uv/utils/property_class_registry.py b/uv_magic_uv/utils/property_class_registry.py
new file mode 100644
index 00000000..20df0342
--- /dev/null
+++ b/uv_magic_uv/utils/property_class_registry.py
@@ -0,0 +1,72 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from .. import common
+
+__all__ = [
+ 'PropertyClassRegistry',
+]
+
+
+class PropertyClassRegistry:
+ class_list = []
+
+ def __init__(self, *_, **kwargs):
+ self.legacy = kwargs.get('legacy', False)
+
+ def __call__(self, cls):
+ PropertyClassRegistry.add_class(cls.idname, cls, self.legacy)
+ return cls
+
+ @classmethod
+ def add_class(cls, idname, prop_class, legacy):
+ for class_ in cls.class_list:
+ if (class_["idname"] == idname) and (class_["legacy"] == legacy):
+ raise RuntimeError("{} is already registered".format(idname))
+
+ new_op = {
+ "idname": idname,
+ "class": prop_class,
+ "legacy": legacy,
+ }
+ cls.class_list.append(new_op)
+ common.debug_print("{} is registered.".format(idname))
+
+ @classmethod
+ def init_props(cls, scene):
+ for class_ in cls.class_list:
+ class_["class"].init_props(scene)
+ common.debug_print("{} is initialized.".format(class_["idname"]))
+
+ @classmethod
+ def del_props(cls, scene):
+ for class_ in cls.class_list:
+ class_["class"].del_props(scene)
+ common.debug_print("{} is cleared.".format(class_["idname"]))
+
+ @classmethod
+ def cleanup(cls):
+ cls.class_list = []
+ common.debug_print("Cleanup registry.")