From 8e579918f0fe5893e56af6f0097414ba1357f81a Mon Sep 17 00:00:00 2001 From: SavMartin Date: Sun, 9 Jun 2019 00:42:50 +0100 Subject: [PATCH] Port to Blender 2.80 --- LICENSE.txt | 24 + __init__.py | 1475 ++++++++++++++++++++++++++++ bake_anti_alias.png | Bin 0 -> 15115 bytes bake_obj_cage.png | Bin 0 -> 17150 bytes bake_obj_float.png | Bin 0 -> 17079 bytes bake_obj_high.png | Bin 0 -> 17055 bytes bake_obj_low.png | Bin 0 -> 16544 bytes op_align.py | 131 +++ op_align_bottom.png | Bin 0 -> 604 bytes op_align_left.png | Bin 0 -> 587 bytes op_align_right.png | Bin 0 -> 608 bytes op_align_top.png | Bin 0 -> 575 bytes op_bake.png | Bin 0 -> 16663 bytes op_bake.py | 581 +++++++++++ op_bake_explode.png | Bin 0 -> 16863 bytes op_bake_explode.py | 221 +++++ op_bake_organize_names.py | 179 ++++ op_color_assign.py | 98 ++ op_color_clear.py | 96 ++ op_color_convert_texture.png | Bin 0 -> 15076 bytes op_color_convert_texture.py | 153 +++ op_color_convert_vertex_colors.png | Bin 0 -> 15587 bytes op_color_convert_vertex_colors.py | 90 ++ op_color_from_directions.png | Bin 0 -> 16269 bytes op_color_from_directions.py | 206 ++++ op_color_from_elements.py | 102 ++ op_color_from_materials.py | 53 + op_color_io_export.py | 42 + op_color_io_import.py | 66 ++ op_color_select.py | 69 ++ op_edge_split_bevel.py | 375 +++++++ op_island_align_edge.py | 142 +++ op_island_align_sort.py | 171 ++++ op_island_align_world.py | 291 ++++++ op_island_mirror.py | 783 +++++++++++++++ op_island_rotate_90.py | 81 ++ op_island_straighten_edge_loops.py | 272 +++++ op_meshtex_create.py | 296 ++++++ op_meshtex_pattern.py | 193 ++++ op_meshtex_trim.py | 77 ++ op_meshtex_trim_collapse.py | 70 ++ op_meshtex_wrap.py | 86 ++ op_rectify.py | 637 ++++++++++++ op_select_islands_flipped.py | 133 +++ op_select_islands_identical.py | 130 +++ op_select_islands_outline.py | 78 ++ op_select_islands_overlap.py | 140 +++ op_smoothing_uv_islands.py | 67 ++ op_texel_checker_map.py | 250 +++++ op_texel_density_get.py | 133 +++ op_texel_density_set.py | 189 ++++ op_texture_open.py | 50 + op_texture_preview.py | 101 ++ op_texture_reload_all.py | 64 ++ op_texture_remove.py | 43 + op_texture_save.py | 109 ++ op_texture_select.py | 84 ++ op_unwrap_edge_peel.py | 95 ++ op_unwrap_faces_iron.py | 76 ++ op_uv_channel_add.py | 67 ++ op_uv_channel_swap.py | 70 ++ op_uv_crop.py | 66 ++ op_uv_fill.py | 117 +++ op_uv_resize.py | 264 +++++ op_uv_size_get.py | 44 + settings.py | 17 + utilities_bake.py | 605 ++++++++++++ utilities_color.py | 227 +++++ utilities_meshtex.py | 120 +++ utilities_texel.py | 138 +++ utilities_ui.py | 170 ++++ utilities_uv.py | 286 ++++++ 72 files changed, 10993 insertions(+) create mode 100644 LICENSE.txt create mode 100644 __init__.py create mode 100644 bake_anti_alias.png create mode 100644 bake_obj_cage.png create mode 100644 bake_obj_float.png create mode 100644 bake_obj_high.png create mode 100644 bake_obj_low.png create mode 100644 op_align.py create mode 100644 op_align_bottom.png create mode 100644 op_align_left.png create mode 100644 op_align_right.png create mode 100644 op_align_top.png create mode 100644 op_bake.png create mode 100644 op_bake.py create mode 100644 op_bake_explode.png create mode 100644 op_bake_explode.py create mode 100644 op_bake_organize_names.py create mode 100644 op_color_assign.py create mode 100644 op_color_clear.py create mode 100644 op_color_convert_texture.png create mode 100644 op_color_convert_texture.py create mode 100644 op_color_convert_vertex_colors.png create mode 100644 op_color_convert_vertex_colors.py create mode 100644 op_color_from_directions.png create mode 100644 op_color_from_directions.py create mode 100644 op_color_from_elements.py create mode 100644 op_color_from_materials.py create mode 100644 op_color_io_export.py create mode 100644 op_color_io_import.py create mode 100644 op_color_select.py create mode 100644 op_edge_split_bevel.py create mode 100644 op_island_align_edge.py create mode 100644 op_island_align_sort.py create mode 100644 op_island_align_world.py create mode 100644 op_island_mirror.py create mode 100644 op_island_rotate_90.py create mode 100644 op_island_straighten_edge_loops.py create mode 100644 op_meshtex_create.py create mode 100644 op_meshtex_pattern.py create mode 100644 op_meshtex_trim.py create mode 100644 op_meshtex_trim_collapse.py create mode 100644 op_meshtex_wrap.py create mode 100644 op_rectify.py create mode 100644 op_select_islands_flipped.py create mode 100644 op_select_islands_identical.py create mode 100644 op_select_islands_outline.py create mode 100644 op_select_islands_overlap.py create mode 100644 op_smoothing_uv_islands.py create mode 100644 op_texel_checker_map.py create mode 100644 op_texel_density_get.py create mode 100644 op_texel_density_set.py create mode 100644 op_texture_open.py create mode 100644 op_texture_preview.py create mode 100644 op_texture_reload_all.py create mode 100644 op_texture_remove.py create mode 100644 op_texture_save.py create mode 100644 op_texture_select.py create mode 100644 op_unwrap_edge_peel.py create mode 100644 op_unwrap_faces_iron.py create mode 100644 op_uv_channel_add.py create mode 100644 op_uv_channel_swap.py create mode 100644 op_uv_crop.py create mode 100644 op_uv_fill.py create mode 100644 op_uv_resize.py create mode 100644 op_uv_size_get.py create mode 100644 settings.py create mode 100644 utilities_bake.py create mode 100644 utilities_color.py create mode 100644 utilities_meshtex.py create mode 100644 utilities_texel.py create mode 100644 utilities_ui.py create mode 100644 utilities_uv.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0baa8d8 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,24 @@ +# Blender TexTools, +# +# Copyright (C) <2018> + +# 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 3 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, see . + + +# Credits +# +# UVSquares: +# +# Copyright (C) <2014> +# https://github.com/JoseConseco/UvSquares/blob/master/uv_squares.py \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d1d6f4b --- /dev/null +++ b/__init__.py @@ -0,0 +1,1475 @@ +bl_info = { + "name": "TexTools", + "description": "Professional UV and Texture tools for Blender.", + "author": "renderhjs, (Port to 2.80 by Sav Martin)", + "version": (1, 3, 00), + "blender": (2, 80, 0), + "category": "UV", + "location": "UV Image Editor > Tools > 'TexTools' panel", + "wiki_url": "http://renderhjs.net/textools/blender/" +} + + +# Import local modules +# More info: https://wiki.blender.org/index.php/Dev:Py/Scripts/Cookbook/Code_snippets/Multi-File_packages +if "bpy" in locals(): + import imp + imp.reload(utilities_ui) + imp.reload(settings) + imp.reload(utilities_bake) + imp.reload(utilities_color) + imp.reload(utilities_texel) + imp.reload(utilities_uv) + imp.reload(utilities_meshtex) + + imp.reload(op_align) + imp.reload(op_bake) + imp.reload(op_bake_explode) + imp.reload(op_bake_organize_names) + imp.reload(op_texture_preview) + imp.reload(op_color_assign) + imp.reload(op_color_clear) + imp.reload(op_color_convert_texture) + imp.reload(op_color_convert_vertex_colors) + imp.reload(op_edge_split_bevel) + imp.reload(op_color_from_elements) + imp.reload(op_color_from_materials) + imp.reload(op_color_from_directions) + imp.reload(op_color_io_export) + imp.reload(op_color_io_import) + imp.reload(op_color_select) + imp.reload(op_island_align_edge) + imp.reload(op_island_align_sort) + imp.reload(op_island_align_world) + imp.reload(op_island_mirror) + imp.reload(op_island_rotate_90) + imp.reload(op_island_straighten_edge_loops) + imp.reload(op_rectify) + imp.reload(op_select_islands_identical) + imp.reload(op_select_islands_outline) + imp.reload(op_select_islands_overlap) + imp.reload(op_select_islands_flipped) + imp.reload(op_smoothing_uv_islands) + imp.reload(op_meshtex_create) + imp.reload(op_meshtex_wrap) + imp.reload(op_meshtex_trim) + imp.reload(op_meshtex_trim_collapse) + imp.reload(op_meshtex_pattern) + imp.reload(op_texel_checker_map) + imp.reload(op_texel_density_get) + imp.reload(op_texel_density_set) + imp.reload(op_texture_reload_all) + imp.reload(op_texture_save) + imp.reload(op_texture_open) + imp.reload(op_texture_select) + imp.reload(op_texture_remove) + imp.reload(op_unwrap_faces_iron) + imp.reload(op_unwrap_edge_peel) + imp.reload(op_uv_channel_add) + imp.reload(op_uv_channel_swap) + imp.reload(op_uv_crop) + imp.reload(op_uv_fill) + imp.reload(op_uv_resize) + imp.reload(op_uv_size_get) + + +else: + from . import settings + from . import utilities_ui + from . import utilities_bake + from . import utilities_color + from . import utilities_texel + from . import utilities_uv + from . import utilities_meshtex + + from . import op_align + from . import op_bake + from . import op_bake_explode + from . import op_bake_organize_names + from . import op_texture_preview + from . import op_color_assign + from . import op_color_clear + from . import op_color_convert_texture + from . import op_color_convert_vertex_colors + from . import op_color_from_elements + from . import op_color_from_materials + from . import op_color_from_directions + from . import op_edge_split_bevel + from . import op_color_io_export + from . import op_color_io_import + from . import op_color_select + from . import op_island_align_edge + from . import op_island_align_sort + from . import op_island_align_world + from . import op_island_mirror + from . import op_island_rotate_90 + from . import op_island_straighten_edge_loops + from . import op_rectify + from . import op_select_islands_identical + from . import op_select_islands_outline + from . import op_select_islands_overlap + from . import op_select_islands_flipped + from . import op_smoothing_uv_islands + from . import op_meshtex_create + from . import op_meshtex_wrap + from . import op_meshtex_trim + from . import op_meshtex_trim_collapse + from . import op_meshtex_pattern + from . import op_texel_checker_map + from . import op_texel_density_get + from . import op_texel_density_set + from . import op_texture_reload_all + from . import op_texture_save + from . import op_texture_open + from . import op_texture_select + from . import op_texture_remove + from . import op_unwrap_faces_iron + from . import op_unwrap_edge_peel + from . import op_uv_channel_add + from . import op_uv_channel_swap + from . import op_uv_crop + from . import op_uv_fill + from . import op_uv_resize + from . import op_uv_size_get + + +# Import general modules. Important: must be placed here and not on top +import bpy +import os +import math +import string +import bpy.utils.previews + +from bpy.types import Menu, Operator, Panel, UIList + +from bpy.props import ( + StringProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + EnumProperty, + PointerProperty, +) + + + +class Panel_Preferences(bpy.types.AddonPreferences): + bl_idname = __name__ + + # Addon Preferences https://docs.blender.org/api/blender_python_api_2_67_release/bpy.types.AddonPreferences.html + swizzle_y_coordinate : bpy.props.EnumProperty(items= + [ + ('Y+', 'Y+ OpenGL', 'Used in Blender, Maya, Modo, Toolbag, Unity'), + ('Y-', 'Y- Direct X', 'Used in 3ds Max, CryENGINE, Source, Unreal Engine') + ], + description="Color template", + name = "Swizzle Coordinates", + default = 'Y+' + ) + bake_32bit_float : bpy.props.EnumProperty(items= + [ + ('8', '8 Bit', ''), + ('32', '32 Bit', '') + ], + description="", + name = "Image depth", + default = '8' + ) + + def draw(self, context): + layout = self.layout + + box = layout.box() + col = box.column(align=True) + col.prop(self, "swizzle_y_coordinate", icon='ORIENTATION_GLOBAL') + if self.swizzle_y_coordinate == 'Y+': + col.label(text="Y+ used in: Blender, Maya, Modo, Toolbag, Unity") + elif self.swizzle_y_coordinate == 'Y-': + col.label(text="Y- used in: 3ds Max, CryENGINE, Source, Unreal Engine") + + box.separator() + col = box.column(align=True) + col.prop(self, "bake_32bit_float", icon='IMAGE_RGB') + if self.bake_32bit_float == '8': + col.label(text="8 Bit images are used. Banding may appear in normal maps.") + elif self.bake_32bit_float == '32': + col.label(text="32 Bit images are used. Images may require dithering to 8 bit.") + + + if not hasattr(bpy.types,"ShaderNodeBevel"): + box.separator() + col = box.column(align=True) + + col.label(text="Unlock Bevel Shader", icon='ERROR') + col.operator("wm.url_open", text="Get Blender with Bevel Shader", icon='BLENDER').url = "https://builder.blender.org/download/" + col.label(text="Use nightly builds of Blender 2.79 or 2.8 to access Bevel baking") + + + + box = layout.box() + + box.label(text="Additional Links") + col = box.column(align=True) + col.operator("wm.url_open", text="Donate", icon='HELP').url = "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ZC9X4LE7CPQN6" + col.operator("wm.url_open", text="GIT Code", icon='WORDWRAP_ON').url = "https://bitbucket.org/renderhjs/textools-blender/src" + + col.label(text="Discussions") + row = col.row(align=True) + row.operator("wm.url_open", text="BlenderArtists", icon='BLENDER').url = "https://blenderartists.org/forum/showthread.php?443182-TexTools-for-Blender" + row.operator("wm.url_open", text="Polycount").url = "http://polycount.com/discussion/197226/textools-for-blender" + row.operator("wm.url_open", text="Twitter").url = "https://twitter.com/search?q=%23textools" + + + +class UV_OT_op_debug(bpy.types.Operator): + bl_idname = "uv.op_debug" + bl_label = "Debug" + bl_description = "Open console and enable dbug mode" + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + bpy.app.debug = True# Debug Vertex indexies + bpy.context.object.data.show_extra_indices = True + bpy.app.debug_value = 1 #Set to Non '0 + return {'FINISHED'} + + + +class UV_OT_op_disable_uv_sync(bpy.types.Operator): + bl_idname = "uv.op_disable_sync" + bl_label = "Disable Sync" + bl_description = "Disable UV sync mode" + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + bpy.context.scene.tool_settings.use_uv_select_sync = False + bpy.ops.mesh.select_all(action='SELECT') + return {'FINISHED'} + + + + +class UV_OT_op_select_bake_set(bpy.types.Operator): + bl_idname = "uv.op_select_bake_set" + bl_label = "Select" + bl_description = "Select this bake set in scene" + + select_set : bpy.props.StringProperty(default="") + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + print("Set: "+self.select_set) + if self.select_set != "": + for set in settings.sets: + if set.name == self.select_set: + # Select this entire set + bpy.ops.object.select_all(action='DESELECT') + for obj in set.objects_low: + obj.select_set( state = True, view_layer = None) + for obj in set.objects_high: + obj.select_set( state = True, view_layer = None) + for obj in set.objects_cage: + obj.select_set( state = True, view_layer = None) + # Set active object to low poly to better visualize high and low wireframe color + if len(set.objects_low) > 0: + bpy.context.view_layer.objects.active = set.objects_low[0] + + break + return {'FINISHED'} + + + +class UV_OT_op_select_bake_type(bpy.types.Operator): + bl_idname = "uv.op_select_bake_type" + bl_label = "Select" + bl_description = "Select bake objects of this type" + + select_type : bpy.props.StringProperty(default='low') + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + objects = [] + for set in settings.sets: + if self.select_type == 'low': + objects+=set.objects_low + elif self.select_type == 'high': + objects+=set.objects_high + elif self.select_type == 'cage': + objects+=set.objects_cage + elif self.select_type == 'float': + objects+=set.objects_float + elif self.select_type == 'issue' and set.has_issues: + objects+=set.objects_low + objects+=set.objects_high + objects+=set.objects_cage + objects+=set.objects_float + + bpy.ops.object.select_all(action='DESELECT') + for obj in objects: + obj.select_set( state = True, view_layer = None) + + return {'FINISHED'} + + + +def on_dropdown_size(self, context): + # Help: http://elfnor.com/drop-down-and-button-select-menus-for-blender-operator-add-ons.html + size = int(bpy.context.scene.texToolsSettings.size_dropdown) + bpy.context.scene.texToolsSettings.size[0] = size; + bpy.context.scene.texToolsSettings.size[1] = size; + + if size <= 128: + bpy.context.scene.texToolsSettings.padding = 2 + elif size <= 512: + bpy.context.scene.texToolsSettings.padding = 4 + else: + bpy.context.scene.texToolsSettings.padding = 8 + + + +def on_dropdown_uv_channel(self, context): + if bpy.context.active_object != None: + if bpy.context.active_object.type == 'MESH': + if bpy.context.object.data.uv_layers: + + # Change Mesh UV Channel + index = int(bpy.context.scene.texToolsSettings.uv_channel) + if index < len(bpy.context.object.data.uv_layers): + bpy.context.object.data.uv_layers.active_index = index + bpy.context.object.data.uv_layers[index].active_render = True + + + +def on_color_changed(self, context): + for i in range(0, context.scene.texToolsSettings.color_ID_count): + utilities_color.assign_color(i) + + + +def on_color_dropdown_template(self, context): + # Change Mesh UV Channel + hex_colors = bpy.context.scene.texToolsSettings.color_ID_templates.split(',') + bpy.context.scene.texToolsSettings.color_ID_count = len(hex_colors) + + # Assign color slots + for i in range(0, len(hex_colors)): + color = utilities_color.hex_to_color("#"+hex_colors[i]) + utilities_color.set_color(i, color) + utilities_color.assign_color(i) + + + +def on_color_count_changed(self, context): + if bpy.context.active_object != None: + utilities_color.validate_face_colors(bpy.context.active_object) + + + +def get_dropdown_uv_values(self, context): + # Requires mesh and UV data + if bpy.context.active_object != None: + if bpy.context.active_object.type == 'MESH': + if bpy.context.object.data.uv_layers: + options = [] + step = 0 + for uvLoop in bpy.context.object.data.uv_layers: + # options.append((str(step), "#{} {}".format(step+1, uvLoop.name), "Change UV channel to '{}'".format(uvLoop.name), step)) + options.append((str(step), "UV {}".format(step+1), "Change UV channel to '{}'".format(uvLoop.name), step)) + step+=1 + + return options + return [] + + + +def on_slider_meshtexture_wrap(self, context): + value = bpy.context.scene.texToolsSettings.meshtexture_wrap + obj_uv = utilities_meshtex.find_uv_mesh(bpy.context.selected_objects) + if obj_uv: + obj_uv.data.shape_keys.key_blocks["model"].value = value + + + +class TexToolsSettings(bpy.types.PropertyGroup): + #Width and Height + size : bpy.props.IntVectorProperty( + name = "Size", + size=2, + description="Texture & UV size in pixels", + default = (512,512), + subtype = "XYZ" + ) + size_dropdown : bpy.props.EnumProperty( + items = utilities_ui.size_textures, + name = "Texture Size", + update = on_dropdown_size, + default = '512' + ) + uv_channel : bpy.props.EnumProperty( + items = get_dropdown_uv_values, + name = "UV", + update = on_dropdown_uv_channel + ) + padding : bpy.props.IntProperty( + name = "Padding", + description="padding size in pixels", + default = 4, + min = 0, + max = 256 + ) + bake_samples : bpy.props.FloatProperty( + name = "Samples", + description = "Samples in Cycles for Baking. The higher the less noise. Default: 64", + default = 8, + min = 1, + max = 4000 + ) + bake_curvature_size : bpy.props.IntProperty( + name = "Curvature", + description = "Curvature offset in pixels to process", + default = 1, + min = 1, + max = 64 + ) + bake_wireframe_size : bpy.props.FloatProperty( + name = "Thickness", + description = "Wireframe Thickness in pixels", + default = 1, + min = 0.1, + max = 4.0 + ) + bake_bevel_size : bpy.props.FloatProperty( + name = "Radius", + description = "Bevel radius 1 to 16", + default = 0.05, + min = 0.0, + max = 1.0 + ) + bake_bevel_samples : bpy.props.IntProperty( + name = "Bevel Samples", + description = "Bevel Samples", + default = 4, + min = 1, + max = 16 + ) + + + bake_ray_distance : bpy.props.FloatProperty( + name = "Ray Dist.", + description = "Ray distance when baking. When using cage used as extrude distance", + default = 0.01, + min = 0.000, + max = 100.00 + ) + bake_force_single : bpy.props.BoolProperty( + name="Single Texture", + description="Force a single texture bake accross all selected objects", + default = False + ) + bake_sampling : bpy.props.EnumProperty(items= + [('1', 'None', 'No Anti Aliasing (Fast)'), + ('2', '2x', 'Render 2x and downsample'), + ('4', '4x', 'Render 2x and downsample')], name = "AA", default = '1' + ) + bake_freeze_selection : bpy.props.BoolProperty( + name="Lock", + description="Lock baking sets, don't change with selection", + default = False + ) + texel_mode_scale : bpy.props.EnumProperty(items= + [('ISLAND', 'Islands', 'Scale UV islands to match Texel Density'), + ('ALL', 'Combined', 'Scale all UVs together to match Texel Density')], + name = "Mode", + default = 'ISLAND' + ) + texel_density : bpy.props.FloatProperty( + name = "Texel", + description = "Texel size or Pixels per 1 unit ratio", + default = 256, + min = 0.0 + # max = 100.00 + ) + meshtexture_wrap : bpy.props.FloatProperty( + name = "Wrap", + description = "Transition of mesh texture wrap", + default = 0, + min = 0, + max = 1, + update = on_slider_meshtexture_wrap, + subtype = 'FACTOR' + ) + + def get_color(hex = "808080"): + return bpy.props.FloatVectorProperty( + name="Color1", + description="Set Color 1 for the Palette", + subtype="COLOR", + default=utilities_color.hex_to_color(hex), + size=3, + max=1.0, min=0.0, + update=on_color_changed + )#, update=update_color_1 + + # 10 Color ID's + color_ID_color_0 : get_color(hex="#ff0000") + color_ID_color_1 : get_color(hex="#0000ff") + color_ID_color_2 : get_color(hex="#00ff00") + color_ID_color_3 : get_color(hex="#ffff00") + color_ID_color_4 : get_color(hex="#00ffff") + color_ID_color_5 : get_color() + color_ID_color_6 : get_color() + color_ID_color_7 : get_color() + color_ID_color_8 : get_color() + color_ID_color_9 : get_color() + color_ID_color_10 : get_color() + color_ID_color_11 : get_color() + color_ID_color_12 : get_color() + color_ID_color_13 : get_color() + color_ID_color_14 : get_color() + color_ID_color_15 : get_color() + color_ID_color_16 : get_color() + color_ID_color_17 : get_color() + color_ID_color_18 : get_color() + color_ID_color_19 : get_color() + + color_ID_templates : bpy.props.EnumProperty(items= + [ + ('3d3d3d,7f7f7f,b8b8b8,ffffff', '4 Gray', '...'), + ('003153,345d4b,688a42,9db63a,d1e231', '5 Greens', '...'), + ('ff0000,0000ff,00ff00,ffff00,00ffff', '5 Code', '...'), + ('3a4342,2e302f,242325,d5cc9e,d6412b', '5 Sea Wolf', '...'), + ('7f87a0,2d3449,000000,ffffff,f99c21', '5 Mustang', '...'), + ('143240,209d8c,fed761,ffab56,fb6941', '5 Sunset', '...'), + ('0087ed,23ca7a,eceb1d,e37a29,da1c2c', '5 Heat', '...'), + ('9e00af,7026b9,4f44b5,478bf4,39b7d5,229587,47b151,9dcf46,f7f235,f7b824,f95f1e,c5513c,78574a,4d4b4b,9d9d9d', '15 Rainbow', '...') + ], + description="Color template", + name = "Preset", + update = on_color_dropdown_template, + default = 'ff0000,0000ff,00ff00,ffff00,00ffff' + ) + + color_ID_count : bpy.props.IntProperty( + name = "Count", + description="Number of color IDs", + default = 5, + update = on_color_count_changed, + min = 2, + max = 20 + ) + + # bake_do_save = bpy.props.BoolProperty( + # name="Save", + # description="Save the baked texture?", + # default = False) + + + +class UI_PT_Panel_Units(bpy.types.Panel): + bl_label = " " + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'UI' + bl_category = "TexTools" + bl_options = {'HIDE_HEADER'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="Size: {} x {}".format(bpy.context.scene.texToolsSettings.size[0], bpy.context.scene.texToolsSettings.size[1])) + + def draw(self, context): + layout = self.layout + + if bpy.app.debug_value != 0: + row = layout.row() + row.alert =True + row.operator("uv.op_debug", text="DEBUG", icon="CONSOLE") + + #---------- Settings ------------ + # row = layout.row() + col = layout.column(align=True) + r = col.row(align = True) + r.prop(context.scene.texToolsSettings, "size_dropdown", text="Size") + r.operator(op_uv_size_get.op.bl_idname, text="", icon = 'EYEDROPPER') + + r = col.row(align = True) + r.prop(context.scene.texToolsSettings, "size", text="") + + r = col.row(align = True) + r.prop(context.scene.texToolsSettings, "padding", text="Padding") + r.operator(op_uv_resize.op.bl_idname, text="Resize", icon_value = icon_get("op_extend_canvas_open")) + + + # col.operator(op_extend_canvas.op.bl_idname, text="Resize", icon_value = icon_get("op_extend_canvas")) + + + # UV Channel + + row = layout.row() + + has_uv_channel = False + if bpy.context.active_object and len(bpy.context.selected_objects) == 1: + if bpy.context.active_object in bpy.context.selected_objects: + if bpy.context.active_object.type == 'MESH': + + # split = row.split(percentage=0.25) + # c = row.column(align=True) + # r = row.row(align=True) + # r.alignment = 'RIGHT' + # r.expand = + # row.label(text="UV")#, icon='GROUP_UVS' + + + if not bpy.context.object.data.uv_layers: + # c = split.column(align=True) + # row = c.row(align=True) + # row.label(text="None", icon= 'ERROR') + + row.operator(op_uv_channel_add.op.bl_idname, text="Add", icon = 'REMOVE') + else: + # c = split.column(align=True) + # row = c.row(align=True) + group = row.row(align=True) + group.prop(context.scene.texToolsSettings, "uv_channel", text="") + group.operator(op_uv_channel_add.op.bl_idname, text="", icon = 'ADD') + + # c = split.column(align=True) + # row = c.row(align=True) + # row.alignment = 'RIGHT' + group = row.row(align=True) + r = group.column(align=True) + r.active = bpy.context.object.data.uv_layers.active_index > 0 + r.operator(op_uv_channel_swap.op.bl_idname, text="", icon = 'TRIA_UP_BAR').is_down = False; + + r = group.column(align=True) + r.active = bpy.context.object.data.uv_layers.active_index < (len(bpy.context.object.data.uv_layers)-1) + r.operator(op_uv_channel_swap.op.bl_idname, text="", icon = 'TRIA_DOWN_BAR').is_down = True; + + has_uv_channel = True + if not has_uv_channel: + row.label(text="UV") + + + col = layout.column(align=True) + + # col.separator() + col.operator(op_texture_reload_all.op.bl_idname, text="Reload Textures", icon_value = icon_get("op_texture_reload_all")) + + row = col.row(align=True) + row.scale_y = 1.75 + row.operator(op_texel_checker_map.op.bl_idname, text ="Checker Map", icon_value = icon_get("op_texel_checker_map")) + + + + + +class UI_PT_Panel_Layout(bpy.types.Panel): + bl_label = " " + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'UI' + bl_category = "TexTools" + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + row = layout.row(align=True) + row.operator("wm.url_open", text="", icon='INFO').url = "http://renderhjs.net/textools/blender/index.html#uvlayout" + row.label(text ="UV Layout") + + # def draw_header(self, _): + # layout = self.layout + # layout.label(text="", icon_value=icon("logo")) + + + def draw(self, context): + layout = self.layout + + + if bpy.app.debug_value != 0: + col = layout.column(align=True) + col.alert = True + row = col.row(align=True) + row.operator(op_island_mirror.op.bl_idname, text="Mirror", icon_value = icon_get("op_island_mirror")).is_stack = False; + row.operator(op_island_mirror.op.bl_idname, text="Stack", icon_value = icon_get("op_island_mirror")).is_stack = True; + + #---------- Layout ------------ + # layout.label(text="Layout") + + box = layout.box() + col = box.column(align=True) + + if bpy.context.active_object and bpy.context.active_object.mode == 'EDIT' and bpy.context.scene.tool_settings.use_uv_select_sync: + row = col.row(align=True) + row.alert = True + row.operator("uv.op_disable_uv_sync", text="Disable sync", icon='CANCEL')#, icon='UV_SYNC_SELECT' + + + row = col.row(align=True) + row.operator(op_uv_crop.op.bl_idname, text="Crop", icon_value = icon_get("op_uv_crop")) + row.operator(op_uv_fill.op.bl_idname, text="Fill", icon_value = icon_get("op_uv_fill")) + + + row = col.row(align=True) + row.operator(op_island_align_edge.op.bl_idname, text="Align Edge", icon_value = icon_get("op_island_align_edge")) + + row = col.row(align=True) + row.operator(op_island_align_world.op.bl_idname, text="Align World", icon_value = icon_get("op_island_align_world")) + + + if bpy.app.debug_value != 0: + c = col.column(align=True) + c.alert = True + + c.operator(op_edge_split_bevel.op.bl_idname, text="Split Bevel") + + col.separator() + + col_tr = col.column(align=True) + + row = col_tr.row(align=True) + col = row.column(align=True) + col.label(text="") + col.operator(op_align.op.bl_idname, text="←", icon_value = icon_get("op_align_left")).direction = "left" + + col = row.column(align=True) + col.operator(op_align.op.bl_idname, text="↑", icon_value = icon_get("op_align_top")).direction = "top" + col.operator(op_align.op.bl_idname, text="↓", icon_value = icon_get("op_align_bottom")).direction = "bottom" + + col = row.column(align=True) + col.label(text="") + col.operator(op_align.op.bl_idname, text="→", icon_value = icon_get("op_align_right")).direction = "right" + + row = col_tr.row(align=True) + row.operator(op_island_rotate_90.op.bl_idname, text="-90°", icon_value = icon_get("op_island_rotate_90_left")).angle = -math.pi / 2 + row.operator(op_island_rotate_90.op.bl_idname, text="+90°", icon_value = icon_get("op_island_rotate_90_right")).angle = math.pi / 2 + + + + + col = box.column(align=True) + row = col.row(align=True) + op = row.operator(op_island_align_sort.op.bl_idname, text="Sort H", icon_value = icon_get("op_island_align_sort_h")) + op.is_vertical = False; + op.padding = utilities_ui.get_padding() + + op = row.operator(op_island_align_sort.op.bl_idname, text="Sort V", icon_value = icon_get("op_island_align_sort_v")) + op.is_vertical = True; + op.padding = utilities_ui.get_padding() + + + aligned = box.row(align=True) + col = aligned.column(align=True) + + row = col.row(align=True) + row.operator(op_island_straighten_edge_loops.op.bl_idname, text="Straight", icon_value = icon_get("op_island_straighten_edge_loops")) + row.operator(op_rectify.op.bl_idname, text="Rectify", icon_value = icon_get("op_rectify")) + + + col.operator(op_unwrap_edge_peel.op.bl_idname, text="Edge Peel", icon_value = icon_get("op_unwrap_edge_peel")) + + row = col.row(align=True) + row.scale_y = 1.75 + row.operator(op_unwrap_faces_iron.op.bl_idname, text="Iron Faces", icon_value = icon_get("op_unwrap_faces_iron")) + + + col.separator() + + # col = box.column(align=True) + row = col.row(align=True) + row.label(text="" , icon_value = icon_get("texel_density")) + row.separator() + row.prop(context.scene.texToolsSettings, "texel_density", text="") + row.operator(op_texel_density_get.op.bl_idname, text="", icon = 'EYEDROPPER') + + row = col.row(align=True) + row.operator(op_texel_density_set.op.bl_idname, text="Apply", icon = 'FACESEL') + row.prop(context.scene.texToolsSettings, "texel_mode_scale", text = "", expand=False) + + #---------- Selection ------------ + + + # /box = layout.box() + # box.label(text="Select") + # col = box.column(align=True) + col.separator() + + row = col.row(align=True) + row.operator(op_select_islands_identical.op.bl_idname, text="Similar", icon_value = icon_get("op_select_islands_identical")) + row.operator(op_select_islands_overlap.op.bl_idname, text="Overlap", icon_value = icon_get("op_select_islands_overlap")) + + row = col.row(align=True) + row.operator(op_select_islands_outline.op.bl_idname, text="Bounds", icon_value = icon_get("op_select_islands_outline")) + row.operator(op_select_islands_flipped.op.bl_idname, text="Flipped", icon_value = icon_get('op_select_islands_flipped')) + + col.separator() + col.operator(op_smoothing_uv_islands.op.bl_idname, text="UV Smoothing", icon_value = icon_get("op_smoothing_uv_islands")) + + +class UI_PT_Panel_Bake(bpy.types.Panel): + bl_label = " " + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'UI' + bl_category = "TexTools" + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + row = layout.row(align=True) + row.operator("wm.url_open", text="", icon='INFO').url = "http://renderhjs.net/textools/blender/index.html#texturebaking" + row.label(text ="Baking") + + def draw(self, context): + layout = self.layout + + #----------- Baking ------------- + row = layout.row() + box = row.box() + col = box.column(align=True) + + if not (bpy.context.scene.texToolsSettings.bake_freeze_selection and len(settings.sets) > 0): + # Update sets + settings.sets = utilities_bake.get_bake_sets() + + + # Bake Button + count = 0 + if bpy.context.scene.texToolsSettings.bake_force_single and len(settings.sets) > 0: + count = 1 + else: + count = len(settings.sets) + + row = col.row(align=True) + row.scale_y = 1.75 + row.operator(op_bake.op.bl_idname, text = "Bake {}x".format(count), icon_value = icon_get("op_bake")); + + # anti aliasing + col.prop(context.scene.texToolsSettings, "bake_sampling", icon_value =icon_get("bake_anti_alias")) + + if bpy.app.debug_value != 0: + row = col.row() + row.alert = True + row.prop(context.scene.texToolsSettings, "bake_force_single", text="Dither Floats") + + col.separator() + + + # Collected Related Textures + col.separator() + + row = col.row(align=True) + row.scale_y = 1.5 + row.operator(op_texture_preview.op.bl_idname, text = "Preview Texture", icon_value = icon_get("op_texture_preview")); + + images = utilities_bake.get_baked_images(settings.sets) + + if len(images) > 0: + + image_background = None + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + if area.spaces[0].image: + image_background = area.spaces[0].image + break + + box = col.box() + # box.label(text="{}x images".format(len(images)), icon="IMAGE_DATA") + col_box = box.column(align=True) + for image in images: + row = col_box.row(align=True) + + # row.label(text=image.name, icon='') + icon = 'RADIOBUT_OFF' + if image == image_background: + icon = 'RADIOBUT_ON' + row.operator(op_texture_select.op.bl_idname, text=image.name, icon=icon).name = image.name #, + + row = row.row(align=True) + row.alignment = 'RIGHT' + if image.filepath != "": + row.operator(op_texture_open.op.bl_idname, text="", icon_value=icon_get("op_texture_open") ).name = image.name + else: + if bpy.app.debug_value != 0: + row.operator(op_texture_save.op.bl_idname, text="", icon_value=icon_get("op_texture_save") ).name = image.name + else: + pass + + row.operator(op_texture_remove.op.bl_idname, text="", icon='X' ).name = image.name + + + col.separator() + + + + # Bake Mode + col.template_icon_view(bpy.context.scene, "TT_bake_mode") + + + if bpy.app.debug_value != 0: + row = col.row() + row.label(text="--> Mode: '{}'".format(bpy.context.scene.TT_bake_mode)) + + bake_mode = utilities_ui.get_bake_mode() + + # Warning: Wrong bake mode, require + if bake_mode == 'diffuse': + if bpy.context.scene.render.engine != 'CYCLES': + if bpy.context.scene.render.engine != op_bake.modes[bake_mode].engine: + col.label(text="Requires '{}'".format(op_bake.modes[bake_mode].engine), icon='ERROR') + + + + + # Optional Parameters + col.separator() + for set in settings.sets: + if len(set.objects_low) > 0 and len(set.objects_high) > 0: + col.prop(context.scene.texToolsSettings, "bake_ray_distance") + break + + # Display Bake mode properties / parameters + if bake_mode in op_bake.modes: + params = op_bake.modes[bake_mode].params + if len(params) > 0: + for param in params: + col.prop(context.scene.texToolsSettings, param) + + # Warning about projection requirement + if len(settings.sets) > 0 and op_bake.modes[bake_mode].use_project == True: + if len(settings.sets[0].objects_low) == 0 or len(settings.sets[0].objects_high) == 0: + col.label(text="Need high and low", icon='ERROR') + + + + box = layout.box() + col = box.column(align=True) + + # Select by type + if len(settings.sets) > 0: + row = col.row(align=True) + row.active = len(settings.sets) > 0 + + count_types = { + 'low':0, 'high':0, 'cage':0, 'float':0, 'issue':0, + } + for set in settings.sets: + if set.has_issues: + count_types['issue']+=1 + if len(set.objects_low) > 0: + count_types['low']+=1 + if len(set.objects_high) > 0: + count_types['high']+=1 + if len(set.objects_cage) > 0: + count_types['cage']+=1 + if len(set.objects_float) > 0: + count_types['float']+=1 + + # Freeze Selection + c = row.column(align=True) + c.active = len(settings.sets) > 0 or bpy.context.scene.texToolsSettings.bake_freeze_selection + icon = 'LOCKED' if bpy.context.scene.texToolsSettings.bake_freeze_selection else 'UNLOCKED' + c.prop(context.scene.texToolsSettings, "bake_freeze_selection",text="Lock {}x".format(len(settings.sets)), icon=icon) + + # Select by type + if count_types['issue'] > 0: + row.operator("uv.op_select_bake_type", text = "", icon = 'ERROR').select_type = 'issue' + + row.operator("uv.op_select_bake_type", text = "", icon_value = icon_get("bake_obj_low")).select_type = 'low' + row.operator("uv.op_select_bake_type", text = "", icon_value = icon_get("bake_obj_high")).select_type = 'high' + + if count_types['float'] > 0: + row.operator("uv.op_select_bake_type", text = "", icon_value = icon_get("bake_obj_float")).select_type = 'float' + + if count_types['cage'] > 0: + row.operator("uv.op_select_bake_type", text = "", icon_value = icon_get("bake_obj_cage")).select_type = 'cage' + + # List bake sets + box2 = box.box() + row = box2.row() + split = None + + countTypes = (0 if count_types['low'] == 0 else 1) + (0 if count_types['high'] == 0 else 1) + (0 if count_types['cage'] == 0 else 1) + (0 if count_types['float'] == 0 else 1) + if countTypes > 2: + # More than 3 types, use less space for label + split = row.split(factor=0.45) + else: + # Only 2 or less types, use more space for label + split = row.split(factor=0.55) + + c = split.column(align=True) + for s in range(0, len(settings.sets)): + set = settings.sets[s] + r = c.row(align=True) + r.active = not (bpy.context.scene.texToolsSettings.bake_force_single and s > 0) + + if set.has_issues: + r.operator("uv.op_select_bake_set", text=set.name, icon='ERROR').select_set = set.name + else: + r.operator("uv.op_select_bake_set", text=set.name).select_set = set.name + + + c = split.column(align=True) + for set in settings.sets: + r = c.row(align=True) + r.alignment = "LEFT" + + if len(set.objects_low) > 0: + r.label(text="{}".format(len(set.objects_low)), icon_value = icon_get("bake_obj_low")) + elif count_types['low'] > 0: + r.label(text="") + + if len(set.objects_high) > 0: + r.label(text="{}".format(len(set.objects_high)), icon_value = icon_get("bake_obj_high")) + elif count_types['high'] > 0: + r.label(text="") + + if len(set.objects_float) > 0: + r.label(text="{}".format(len(set.objects_float)), icon_value = icon_get("bake_obj_float")) + elif count_types['float'] > 0: + r.label(text="") + + if len(set.objects_cage) > 0: + r.label(text="{}".format(len(set.objects_cage)), icon_value = icon_get("bake_obj_cage")) + elif count_types['cage'] > 0: + r.label(text="") + + # Force Single + row = box2.row(align=True) + row.active = len(settings.sets) > 0 + row.prop(context.scene.texToolsSettings, "bake_force_single", text="Single Texture") + if len(settings.sets) > 0 and bpy.context.scene.texToolsSettings.bake_force_single: + row.label(text="'{}'".format(settings.sets[0].name)) + # else: + # row.label(text="") + + + + + col = box.column(align=True) + col.operator(op_bake_organize_names.op.bl_idname, text = "Organize {}x".format(len(bpy.context.selected_objects)), icon = 'BOOKMARKS') + col.operator(op_bake_explode.op.bl_idname, text = "Explode", icon_value = icon_get("op_bake_explode")); + + + + + + +class UI_MT_op_color_dropdown_io(bpy.types.Menu): + bl_idname = "UI_MT_op_color_dropdown_io" + bl_label = "IO" + + def draw(self, context): + layout = self.layout + + layout.operator(op_color_io_export.op.bl_idname, text="Export Colors", icon = 'EXPORT') + layout.operator(op_color_io_import.op.bl_idname, text="Import Colors", icon = 'IMPORT') + + + +class UI_MT_op_color_dropdown_convert_from(bpy.types.Menu): + bl_idname = "UI_MT_op_color_dropdown_convert_from" + bl_label = "From" + bl_description = "Create Color IDs from ..." + + def draw(self, context): + layout = self.layout + layout.operator(op_color_from_elements.op.bl_idname, text="Mesh Elements", icon_value = icon_get('op_color_from_elements')) + layout.operator(op_color_from_materials.op.bl_idname, text="Materials", icon_value = icon_get('op_color_from_materials')) + layout.operator(op_color_from_directions.op.bl_idname, text="Directions", icon_value = icon_get('op_color_from_directions')) + + + +class UI_MT_op_color_dropdown_convert_to(bpy.types.Menu): + bl_idname = "UI_MT_op_color_dropdown_convert_to" + bl_label = "To" + bl_description = "Convert Color IDs into ..." + + def draw(self, context): + layout = self.layout + layout.operator(op_color_convert_texture.op.bl_idname, text="Texture Atlas", icon_value = icon_get('op_color_convert_texture')) + layout.operator(op_color_convert_vertex_colors.op.bl_idname, text="Vertex Colors", icon_value = icon_get("op_color_convert_vertex_colors")) + + +class UV_OT_op_enable_cycles(bpy.types.Operator): + bl_idname = "uv.textools_enable_cycles" + bl_label = "Enable Cycles" + bl_description = "Enable Cycles render engine" + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + bpy.context.scene.render.engine = 'CYCLES' + return {'FINISHED'} + + +class UI_PT_Panel_Colors(bpy.types.Panel): + bl_label = " " + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'UI' + bl_category = "TexTools" + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + row = layout.row(align=True) + row.operator("wm.url_open", text="", icon='INFO').url = "http://renderhjs.net/textools/blender/index.html#colorid" + row.label(text ="Color ID") + + def draw(self, context): + layout = self.layout + + # layout.label(text="Select face and color") + + if bpy.context.scene.render.engine != 'CYCLES' and bpy.context.scene.render.engine != 'BLENDER_EEVEE': + row = layout.row(align=True) + row.alert = True + row.operator("uv.op_enable_cycles", text="Enable 'CYCLES'", icon='CANCEL')#, icon='UV_SYNC_SELECT' + return + + + box = layout.box() + col = box.column(align=True) + + + + row = col.row(align=True) + split = row.split(factor=0.60, align=True) + c = split.column(align=True) + c.prop(context.scene.texToolsSettings, "color_ID_templates", text="") + c = split.column(align=True) + c.prop(context.scene.texToolsSettings, "color_ID_count", text="", expand=False) + + row = box.row(align=True) + row.operator(op_color_clear.op.bl_idname, text="Clear", icon = 'X') + row.menu(UI_MT_op_color_dropdown_io.bl_idname, icon='COLOR') + + + max_columns = 5 + if context.scene.texToolsSettings.color_ID_count < max_columns: + max_columns = context.scene.texToolsSettings.color_ID_count + + count = math.ceil(context.scene.texToolsSettings.color_ID_count / max_columns)*max_columns + + for i in range(count): + + if i%max_columns == 0: + row = box.row(align=True) + + col = row.column(align=True) + if i < context.scene.texToolsSettings.color_ID_count: + col.prop(context.scene.texToolsSettings, "color_ID_color_{}".format(i), text="") + col.operator(op_color_assign.op.bl_idname, text="", icon = "FILE_TICK").index = i + + if bpy.context.active_object: + if bpy.context.active_object in bpy.context.selected_objects: + if len(bpy.context.selected_objects) == 1: + if bpy.context.active_object.type == 'MESH': + col.operator(op_color_select.op.bl_idname, text="", icon = "FACESEL").index = i + else: + col.label(text=" ") + + + # split = row.split(percentage=0.25, align=True) + # c = split.column(align=True) + # c.operator(op_color_clear.op.bl_idname, text="", icon = 'X') + # c = split.column(align=True) + # c.operator(op_color_from_elements.op.bl_idname, text="Color Elements", icon_value = icon_get('op_color_from_elements')) + + + + col = box.column(align=True) + col.label(text="Convert") + row = col.row(align=True) + row.menu(UI_MT_op_color_dropdown_convert_from.bl_idname)#, icon='IMPORT' + row.menu(UI_MT_op_color_dropdown_convert_to.bl_idname,)# icon='EXPORT' + + + + + # row = col.row(align=True) + # row.operator(op_color_convert_texture.op.bl_idname, text="From Atlas", icon_value = icon_get('op_color_convert_texture')) + + + + # for i in range(context.scene.texToolsSettings.color_ID_count): + + + + # col = row.column(align=True) + # col.prop(context.scene.texToolsSettings, "color_ID_color_{}".format(i), text="") + # col.operator(op_color_assign.op.bl_idname, text="", icon = "FILE_TICK").index = i + + # if bpy.context.active_object: + # if bpy.context.active_object.type == 'MESH': + # if bpy.context.active_object.mode == 'EDIT': + # col.operator(op_color_select.op.bl_idname, text="", icon = "FACESEL").index = i + + + + # https://github.com/blenderskool/kaleidoscope/blob/fb5cb1ab87a57b46618d99afaf4d3154ad934529/spectrum.py + + + + +class UI_PT_Panel_MeshTexture(bpy.types.Panel): + bl_label = " " + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'UI' + bl_category = "TexTools" + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + row = layout.row(align=True) + row.operator("wm.url_open", text="", icon='INFO').url = "http://renderhjs.net/textools/blender/index.html#meshtexture" + row.label(text ="Mesh Texture") + + def draw(self, context): + layout = self.layout + box = layout.box() + col = box.column(align=True) + + row = col.row(align=True) + row.scale_y = 1.5 + row.operator(op_meshtex_create.op.bl_idname, text="Create UV Mesh", icon_value = icon_get("op_meshtex_create")) + + row = col.row(align=True) + row.operator(op_meshtex_trim.op.bl_idname, text="Trim", icon_value = icon_get("op_meshtex_trim")) + + # Warning about trimmed mesh + if op_meshtex_trim_collapse.is_available(): + row.operator(op_meshtex_trim_collapse.op.bl_idname, text="Collapse Trim", icon_value=icon_get("op_meshtex_trim_collapse")) + + + col = box.column(align=True) + row = col.row(align = True) + row.operator(op_meshtex_wrap.op.bl_idname, text="Wrap", icon_value = icon_get("op_meshtex_wrap")) + + row = col.row(align = True) + if not utilities_meshtex.find_uv_mesh(bpy.context.selected_objects): + row.enabled = False + row.prop(context.scene.texToolsSettings, "meshtexture_wrap", text="Wrap") + + box.operator(op_meshtex_pattern.op.bl_idname, text="Create Pattern", icon_value = icon_get("op_meshtex_pattern")) + + + + + + +keymaps = [] + +def icon_get(name): + return utilities_ui.icon_get(name) + + + +def menu_IMAGE_uvs(self, context): + layout = self.layout + layout.separator() + layout.operator(op_uv_resize.op.bl_idname, text="Resize", icon_value = icon_get("op_extend_canvas_open")) + layout.operator(op_rectify.op.bl_idname, text="Rectify", icon_value = icon_get("op_rectify")) + layout.operator(op_uv_crop.op.bl_idname, text="Crop", icon_value = icon_get("op_uv_crop")) + layout.operator(op_uv_fill.op.bl_idname, text="Crop", icon_value = icon_get("op_uv_fill")) + + layout.separator() + layout.operator(op_island_align_sort.op.bl_idname, text="Sort H", icon_value = icon_get("op_island_align_sort_h")) + layout.operator(op_island_align_sort.op.bl_idname, text="Sort V", icon_value = icon_get("op_island_align_sort_v")) + + layout.separator() + layout.operator(op_island_align_edge.op.bl_idname, text="Align Edge", icon_value = icon_get("op_island_align_edge")) + layout.operator(op_island_align_world.op.bl_idname, text="Align World", icon_value = icon_get("op_island_align_world")) + + layout.menu(VIEW3D_MT_submenu_align) + +class VIEW3D_MT_submenu_align(bpy.types.Menu): + bl_label="Align" + bl_idname="VIEW3D_MT_submenu_align" + def draw(self, context): + layout = self.layout + layout.operator(op_align.op.bl_idname, text="←", icon_value = icon_get("op_align_left")).direction = "left" + layout.operator(op_align.op.bl_idname, text="↑", icon_value = icon_get("op_align_top")).direction = "top" + layout.operator(op_align.op.bl_idname, text="↓", icon_value = icon_get("op_align_bottom")).direction = "bottom" + layout.operator(op_align.op.bl_idname, text="→", icon_value = icon_get("op_align_right")).direction = "right" + +def menu_IMAGE_select(self, context): + layout = self.layout + layout.separator() + layout.operator(op_select_islands_identical.op.bl_idname, text="Similar", icon_value = icon_get("op_select_islands_identical")) + layout.operator(op_select_islands_overlap.op.bl_idname, text="Overlap", icon_value = icon_get("op_select_islands_overlap")) + layout.operator(op_select_islands_outline.op.bl_idname, text="Bounds", icon_value = icon_get("op_select_islands_outline")) + layout.operator(op_select_islands_flipped.op.bl_idname, text="Flipped", icon_value = icon_get('op_select_islands_flipped')) + +def menu_IMAGE_MT_image(self, context): + layout = self.layout + layout.separator() + layout.operator(op_texture_reload_all.op.bl_idname, text="Reload Textures", icon_value = icon_get("op_texture_reload_all")) + layout.operator(op_texel_checker_map.op.bl_idname, text ="Checker Map", icon_value = icon_get("op_texel_checker_map")) + layout.operator(op_texture_preview.op.bl_idname, text = "Preview Texture", icon_value = icon_get("op_texture_preview")); + +def menu_VIEW3D_MT_object(self, context): + self.layout.separator() + self.layout.operator(op_texel_checker_map.op.bl_idname, text ="Checker Map", icon_value = icon_get("op_texel_checker_map")) + self.layout.operator(op_meshtex_create.op.bl_idname, text="Create UV Mesh", icon_value = icon_get("op_meshtex_create")) + +def menu_VIEW3D_MT_mesh_add(self, context): + self.layout.operator(op_meshtex_pattern.op.bl_idname, text="Create Pattern", icon_value = icon_get("op_meshtex_pattern")) + +def menu_VIEW3D_MT_uv_map(self, context): + layout = self.layout + layout.separator() + layout.operator(op_unwrap_edge_peel.op.bl_idname, text="Peel Edge", icon_value = icon_get("op_unwrap_edge_peel")) + layout.operator(op_unwrap_faces_iron.op.bl_idname, text="Iron Faces", icon_value = icon_get("op_unwrap_faces_iron")) + layout.operator(op_smoothing_uv_islands.op.bl_idname, text="UV Smoothing", icon_value = icon_get("op_smoothing_uv_islands")) + +def menu_VIEW3D_MT_object_context_menu(self, context): + layout = self.layout + layout.separator() + layout.operator(op_meshtex_create.op.bl_idname, text="Create UV Mesh", icon_value = icon_get("op_meshtex_create")) + layout.operator(op_meshtex_trim.op.bl_idname, text="Trim", icon_value = icon_get("op_meshtex_trim")) + + # Warning about trimmed mesh + if op_meshtex_trim_collapse.is_available(): + layout.operator(op_meshtex_trim_collapse.op.bl_idname, text="Collapse Trim", icon='CANCEL') + + layout.prop(context.scene.texToolsSettings, "meshtexture_wrap", text="Wrap") + layout.operator(op_meshtex_wrap.op.bl_idname, text="Wrap", icon_value = icon_get("op_meshtex_wrap")) + + +classes = ( + UV_OT_op_debug, + UV_OT_op_disable_uv_sync, + UV_OT_op_select_bake_set, + UV_OT_op_select_bake_type, + TexToolsSettings, + UI_PT_Panel_Units, + UI_PT_Panel_Layout, + UI_PT_Panel_Bake, + UI_MT_op_color_dropdown_io, + UI_MT_op_color_dropdown_convert_from, + UI_MT_op_color_dropdown_convert_to, + UV_OT_op_enable_cycles, + UI_PT_Panel_Colors, + UI_PT_Panel_MeshTexture, + VIEW3D_MT_submenu_align, + Panel_Preferences + +) + + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + +#Register settings + bpy.types.Scene.texToolsSettings = bpy.props.PointerProperty(type=TexToolsSettings) + + #GUI Utilities + utilities_ui.register() + + # Register Icons + icons = [ + "bake_anti_alias.png", + "bake_obj_cage.png", + "bake_obj_float.png", + "bake_obj_high.png", + "bake_obj_low.png", + "op_align_bottom.png", + "op_align_left.png", + "op_align_right.png", + "op_align_top.png", + "op_bake.png", + "op_bake_explode.png", + "op_color_convert_texture.png", + "op_color_convert_vertex_colors.png", + "op_color_from_directions.png", + "op_color_from_elements.png", + "op_color_from_materials.png", + "op_extend_canvas_open.png", + "op_island_align_edge.png", + "op_island_align_sort_h.png", + "op_island_align_sort_v.png", + "op_island_align_world.png", + "op_island_mirror.png", + "op_island_rotate_90_left.png", + "op_island_rotate_90_right.png", + "op_island_straighten_edge_loops.png", + "op_meshtex_create.png", + "op_meshtex_pattern.png", + "op_meshtex_trim.png", + "op_meshtex_trim_collapse.png", + "op_meshtex_wrap.png", + "op_rectify.png", + "op_select_islands_flipped.png", + "op_select_islands_identical.png", + "op_select_islands_outline.png", + "op_select_islands_overlap.png", + "op_smoothing_uv_islands.png", + "op_texel_checker_map.png", + "op_texture_preview.png", + "op_texture_reload_all.png", + "op_texture_save.png", + "op_texture_open.png", + "op_unwrap_faces_iron.png", + "op_unwrap_edge_peel.png", + "op_uv_crop.png", + "op_uv_fill.png", + "texel_density.png" + ] + for icon in icons: + utilities_ui.icon_register(icon) + + bpy.types.IMAGE_MT_uvs.append(menu_IMAGE_uvs) + bpy.types.IMAGE_MT_select.append(menu_IMAGE_select) + bpy.types.IMAGE_MT_image.append(menu_IMAGE_MT_image) + bpy.types.VIEW3D_MT_object.append(menu_VIEW3D_MT_object) + bpy.types.VIEW3D_MT_add.append(menu_VIEW3D_MT_mesh_add) + bpy.types.VIEW3D_MT_uv_map.append(menu_VIEW3D_MT_uv_map) + bpy.types.VIEW3D_MT_object_context_menu.append(menu_VIEW3D_MT_object_context_menu) + + + + +def unregister(): + #GUI Utilities + # utilities_ui.unregister() + + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + + + #Unregister Settings + del bpy.types.Scene.texToolsSettings + + #handle the keymap + for km, kmi in keymaps: + km.keymap_items.remove(kmi) + keymaps.clear() + + bpy.types.IMAGE_MT_uvs.remove(menu_IMAGE_uvs) + bpy.types.IMAGE_MT_select.remove(menu_IMAGE_select) + bpy.types.IMAGE_MT_image.remove(menu_IMAGE_MT_image) + bpy.types.VIEW3D_MT_object.remove(menu_VIEW3D_MT_object) + bpy.types.VIEW3D_MT_add.remove(menu_VIEW3D_MT_mesh_add) + bpy.types.VIEW3D_MT_uv_map.remove(menu_VIEW3D_MT_uv_map) + bpy.types.VIEW3D_MT_object_context_menu.remove(menu_VIEW3D_MT_object_context_menu) + + + +if __name__ == "__main__": + register() diff --git a/bake_anti_alias.png b/bake_anti_alias.png new file mode 100644 index 0000000000000000000000000000000000000000..4ef7c0114a0f0869a3e618881febc4bc1c3df131 GIT binary patch literal 15115 zcmeI3e^3)=8pqclLJ?a>@y3Jc%wFi(YE3pj3?XY0K!OEpDDtD?sWU9Q3+$0()9hjb zTs79Y!5?!K>)JMEmslWK3YEf$NQ+}WwLc5LVL#Gf->o$9@tgybaz z-}a_|zB?20@V?*YdEWgz&--To*)3(IPs~WmOd|+lMoF=w9DI|?Cv_V5E48%T1YZw$ zi&semF@3i3nL_M*Z7xCNJ|a|B%hk^1G%LEb3@6s|+JM^&)C6J83wRl}k(bF@zFzRy z)T19BQIi5^Q?E2Rbxv;~-yjsX`1p#J(n_|ak+pE@JbRioK!X5oUS`OE+vSnyfK45W zO9NXOrqpCeA~)L91xiA)+F3>xiawq+X>&BJUau$37OmctYc!Y^k_MgLMCmM)UZ>IN zX}yj%=*Y;Wwxf`F@a>vq0I`E%O-5|?enxg!Ezt(TmioSYEZ?RY?oq;kKG(e$| znmsZT(0HWmaFDS$4qjq?f>#zq52?guYQ-kmrdBJ7qF3a;+}>y+j}&1CC{h8&OX;;b zDv=Y%Mt!_ZK38aI982*o-pzYt3Ha#~`gt2fS(F;YNm53qCru1$;&euHCtR=FoiMc| z7p(&bB1jW@N|nuCo+{@hvB}5sMeD$=WGA|NS*VK@XIvy@X419u0bvq1WhU$vYK3rx ztl(bhLLbk_qOVdEUG{L7m5m`J3kySCMLxDt@NlAEns24zU1If)MdKZe%-caf=7LVt z=uDORT-s!zP3Fh-dRnK0f`pZEF`ObN)HP3tVF9kRNe9P(l<_gZ{^1yz`AJb=Efj}i z={nKpW@NkIX6kv$>#4U=u|Y@~?JBxZbcsH&EZ%OkQi+BkuTZDaB_2s;JS<=0u!8}u zAaJyfGpQg%xuH1{(@ zL+%qoKjjm^NyoS#jk5Zmn<7D&38{(&3G`<()VH8}LuXk9|IGN&u?d$DI+MJNPvVsw zXH$=Dx#%GlJ);z#(9We9Ryob=tg<0^&Pt6NjV&wc8F3T--vWl?L++F3@HgCFZmxWG>zxFW9bbiC`T`@6!^@vbq?Dvu!B4Uued!;qJ9 z@`jIv3hK-w4=5)mxGxL_Jc8{8Trdr!jd39n!2*m6rh&9EE+isYfN{YzkT%AJL<9>k zE|><=#<-A(U;)Mj(?Hr77ZMRHz_?%XVQ5y1kC3#NgzF)kz`Sb%ZCG?4avaizsx{o_60eLp{Vv9G4*kGZ40MeF5Hq=$BE1~-+@-|_N-+Q#9J zxf^@hdNw<2zcj7hI?|iJKK+J$^kmm)&vzZq&LszhTX)u;xfwk8`Tovhf%L)Pq4ftV z&b)Z@9$mJg{Ze*v>BgiHqt|6rRloUZDcdzvv+dx*0sjV3dwb2HSN0t_TGwMeHs`IO zyl?+;c4e>Fm946(dHuwB^49N%^+enDjQ#Te$=&nXztw`0iyeC{^j&*n>Wr%I?6nldxSW*mOOCQ zv1gxgr)q_2*VV>Tk91@{_xfk@$XZoia97oVeea(ho_40>&(^P*^F?clUpW4?apy-z z`%dESKY~Pomy6J8dhI7e_zahvvo$d%x$Ub?)~fs*XX*{ zSAV#ZHDdGH-y)8kf4=JZ7ryx0@xSsNgYR~nnB_^&U$d)qbny1-8ODmQHtqj>*WqQo zf1m!omK*ui#aXX1!~H{nO9d%c<^|pZ0D}S7mx$*E{PE?QX7=G>Q?mG|W4m`%GHV3D5UAwM%sn4xGUD3ZiC^l!8&Cln! z<=lYPGV|{Aj^4(3nI?QK@79lA7oK2XU9pr~m)} literal 0 HcmV?d00001 diff --git a/bake_obj_cage.png b/bake_obj_cage.png new file mode 100644 index 0000000000000000000000000000000000000000..5b8a21dda5c60bdf4192cf84f9bb84f402864327 GIT binary patch literal 17150 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 N-=xW2`zA&t{{x7@R34zXEV?Y^G7jrY`zb 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 literal 0 HcmV?d00001 diff --git a/bake_obj_high.png b/bake_obj_high.png new file mode 100644 index 0000000000000000000000000000000000000000..c8d1d194acebc1c6c2e6aad498ca3edd219929bc GIT binary patch literal 17055 zcmeI3c~leE9>)i80Ul_jwpF1tja1x1R+11xkWIly*a`wIC_^$KQ^;Z#2|P^EilSEB z>b_%ffueN-s}&VRM6?`B6=<}mh!g=uSpEnST7t}^cV)v4s0>b!L*yVxDN`#1kk=AOs+v07B!q(gIC8ZjuA7fg zK!)NV3F4weCQlu93UaYbB~yxJBdoh$9&z<5STwLXLzZArMs$NT@Ca@OrN^y16?%4v zi{u~2Lji#*nIc#wi{tfVSwP?H2_7EZSw)y03QL8uc;yT(8SOH#-viMgPf!K%kbGn# znMk5Af{}crvp6*7EGm^lp$rPr(-RegkL11(RDnaH&>VwlG!Dg)Lm3ifNMCdunGhBw zyi=5coS{EEY*baiWu`RER}#VpAXz z2(g5Yj&wF1nMdD1gMz&^kiP;(>Jk_?sE=VSLc?09pwI~tFi1i+9oZ`;10+R@q8TJd z8VzC!X#$pj*0W*AT80Vp)==KC63M27L0N|6T6g9799lfIwNHqHyDNwsR45_C!E_<^ zoy`7{(_c{xKHUeL0}2eahbJ(kFC^rWhqMk%tKYNN4IXAY-w8oa{6X#`j}b41q*&?S z6RNw{$U&4ckt!ZkKyEQehP=zk?1?(m4IQ3KHq`1k_&=}_N^r@)w+$>ND2x^|1SH33 z0h>glF=(V{7F|e+h6ErB$#@o%f}E=Vi)|RlVu2Wx#y~5C4gX6!_ipv@U){N3 zD*u1)0R3e<*K-nw*2KP2{&o%J3I!aIOd$bPJXiw8KxDZzhD#o39LN^#z}>?`7AI5i zWkQHY=aPqO9^_?stT5Dfq}G7lbrm^kZ?&QOhZ*X0oVvR|i$*~%NNUl|n@ z3m*gp1lz=MVe>($I4%?rY!kha5Ns32h0O=0;U1lz=MVe>($I4%?rY!k#p>ynG8ukvDGRkymTO?q-%DujLX1-a&i-SY-_WYu^AyCi4J5 zYYzZlCj&s;EdXHc1AtHGMn6*{_86s)r(3Y*_tV=-WY+NHLRUdaTUoilSzUknGv>NA z=V#8aytC3IYxa@!r^_tU#b=(*4|fn3jMkg#Q*7s4P&^27usN4uW$ZX%712{S9ezJ{ z*M^B1uJxL0RWHi-equY$nI7xx>eM*@c-*ZH-t#7IWldqn($V_d2}V|PZtf#e0Gs_| z!}Q!cjok4sCU$rdMn6tA4ruFS`fj;MEy%>XO}1O#b);djo%a% z6{V(67oG|yZoHm4Cs&toUs@_=1cbDxFD!gBwJ|j6gK}pJ%aW#Xj)X@ZogY6q7<9CG zt>UNM4qN8$c8*WeaJ6<3-&_VF%PMrbsykVs_0OK^FO=1F*@m^=rug4jW=b)->9eUh zJR>N4=C)#AfB%Ei%{O|lP;vO2J##0{^Wv9YrC6ljp1vl1LrG0hP17g8eRT#733>n0 zqU7wwcGoPuJ5K1se~MUmPE~7_;%?*>xB=R+dbOF=>QT$(@16A|x=q`BnZXYZsjja$ zm71Pv=TmL(zqd29v^Fg7U)MW^Z5Hl6dcY{QATG`xH-ZPM%%-=w)?rWum^by=IT!;o4OX_bkerI_7b*lUJZt z`!Ij>%l4!{7c+NRZkcy5F#YzQsV{f2WNCyGXU|@5ZDrmPAzYIsy3@Lb*e>mxs)mLJ zmy4e32v6E>RqwhqK1L*(*`BmF!r`N9N##voHIU-|a1LAXlg$f_jaOiP<*d+S0Y`(i z<`GRzadxBN2N&EA$YT`>E1ylNg;m#2emi~5fnQ%-H4*3P#^%+YOSAL28cp>KwA7X*( zcG(NoHr14DE~#;CR+2s-5RAP7C&o0jCrwvv&z-k!-g?^;IXQD@f9O%B4i2#_v-2@8 zxhXnz{UkR#^Q*}LKxA{(qMJfmWm-Y>qTK6LnM;ZD-PEqGr9ecPi();JRJ6&Rt$S_r z?tdqwg|mIdZI>=b$%#`g?>%VYlM&=r@R_j*Q9C|$+$UC=`UKmpEg_Y1o0>4Yh%(Ev zjxN2aLU65cx%syi_p2YSgLKTasuk|{r7Z6CoCc4LavS^Rb7gfY|1hqS=xqP6eY&5dh^RZv?$^0|gi~h%7sn0)u)uB2c3EZew1mt`nxA*LH zwJc?@Ox()UwTQymvvK>=bG+zaib<-jVabu%jatg2Z`s0~(|9m-IZc(kvWjh;y`Ziz zBghQkmu|2#i!3XuR!n`RJzi#Ao4U8=WAAg9qvmAYF`saBLDmo1*|%#|OS-ZWzW;!= z?2Ci9DVA4E1&81%TcHDWuf6ZNlw4W7Tb?#6KJp`esXTI<=#<%|{Cwl$;^HHjuX91^ zdz1E?SRbq7A(NfBwxBUh#@x02(CS0*;h7$~9GJPQInm{53+GrJ&DFK!#tn0wPPg>I zJ~wMy<|SkOeK8|1Y~ey?(9z)QNxr4uFFY;?|FEN@WBZ{5@wzX!retOw$((<&vZg_o zTb{!|5%%~|!CG!o+lIsodiw`Q*S8t@Z8sJz%g72nu77`XIw z=ZAJvsuep@Qbs@f<3e#&;rffesk>M_|0ENOYcqD3C7C6i$jj3*r>xWdeERg&Cr_q5 z|Kq}yCT{DiR})4Vg>N0dA*lG|+#50wT;@CN${y!<&5K7~k!3BF?TsvBnqtzdw6>q$ zEPb$fe#XlRWPU}WQ$HL#wo+`vT2nw|N$*8TBd>FivL9E&?&sfDKw!Eq8vAaYJBDh}Kht6E!I^+4z3wr%@b?!uL$t7f;` zG}<<5i5uGpd;Cg@PI9Sw9&!+u94@W+RlT%geM{!}gr+0gZo?L)?&)nm)LQFzzr0w` zxq{a9`1hs*TKaF##}>NyjrMb09_!q&t-h_U+-B!5@S?(eAK#icE_!F6>@~;TS2w{3 Qxm5u^UVfg(+@qHN6IqS#5C8xG literal 0 HcmV?d00001 diff --git a/bake_obj_low.png b/bake_obj_low.png new file mode 100644 index 0000000000000000000000000000000000000000..4dc44a35aab4f5310caa2e310e185aedd4c6caea GIT binary patch literal 16544 zcmeI3dsGuw9><4A6%kr3+m=;UV|0~vo6O|>Foc2u!8H~ER}g$mG7}gLnV1eICl$KT7ifCJoTBIOaE4Hj__lU0w^?{;2E=#Kg5!;GNWpBd!0&_gO-Lq%+%$$>C ze)o5O-#hnnfA=?&KW0PBoSFW<<9rbW@sEx&#DZUe>*F&5d^6eEKY*W6_NYV#K}H9- zJ|0Nnwm<}#zMG0qU=xh9wPrd^j1#np6g$)Gpf`f(LY;Qpyo_W8CNhPxg$QqdS|t=v zM2K*{!iX8|GssjbD%(NEWzUH>XD>5r2w|w+SLf6Mfi#lE1#ryDUT)<{D6tV2lyWpdF+@Ay$ga5{X2h(ugGrwM?pbQ6R-63KY|z5=?|iv=U4! z#RT1lQ11)QbPmFzjWtAe4+pFeVJgeowJ4gInJLbciD^d)D$!^(C?-XvQW5AOVzO*3 z?iAUW$z38nItG$4J19F#(Kdlg7dO%AY=}_k8mRZ^o>!W^cOV7<4Ub9uVL+^NvDLOPIt z+8GWKXK6<~O)oeD8gQ1>gM3tjOcY`A zc#w}Wl@^o#P9o7_SU-`jo{)wS#55?Z;kzsi8VuntX7maNB8>FYUA z!)Vk-+ZYzNnaOB_9?V@#Q3RNRNs6n?a*EIlUFd731FETHNfa zJ$kb%eMv%x4ruMWtlr4(Flz8}J|2QD{eI!YkCB;5+PKocOUm7AcoBxSu$j1n3`+qS z@;E!QOLbrvEU!adwc1Mk2Nr_CGyc70Ae0iB+N1>ANTXDUlqNYQ0^1Cf5L_WuDshV( zlY?aczgUL8EHyLJR6Us5Q8| zu1a+1pfWW37dg*Hv3hCuXi#&%DA%)( z)(s;LJf@R+CAf7ebST^mN5oK0(wbnPz#W=#-DzZCBzFw#7&0(%xWImn?!c~8==VVF zuFhSp;Bg)NQ$TzFDhw*zz4y0Y@}zG@zoaUp#Sa1j;Wl|(+ z$>ZYY19^E|5D;#Y$HmPD^76PKAlxR8i<=MR<#9nkxJ@1xHy_B$;RVK9HBk z1p(nUd0gClATN�>W+bxVZU1ULF?&gxlnCar1$^JT3?bx5=L3 z-d)%d0ba`$n4@Bi2(oTGf^6zUJiKNi$RDR6NNyg2+&YCIs&^4&QlzOR9oXLB9lWr3 z=jED$PC6hx@Wf)#Owm%bG0=bdfsADlg_AmvzdS45gr&^hwrXPSl9&xu4@z>^gg$ts z?Wy(AMUxCqV+*%pRig2)5($xSm1Y!Yeo{Z>!tT8V4b7()@7rCHr1|=t_Ku8#lTBHn z`*U_Q-PKK~ukdJf>MAj`RVVO5J@8|mzQ;V@^Yl+HOsn^)R82hFq@Hl+;m`C5Q|Ww< z3HlZ7pYFSL&8tcsEFXr61qelK%)md&Tbsk9;=lXXb zHHVA8`L1pBk-Tf4hxi?t;N#u!+Hupv-$t8QcI(ljdL(`xX)<{l&nF}&AK7~=`?9}( zeTC|ovzs68x^_EsI)-iW8}Z7Nr$;?GQQxt@y71K0xVSi-vo+6D+Fmez-1t$gw$(}N zBa3#$KEFc0c>b<4;YD$I*9|}Wwyq7{IML7Fe@*7?hEpHL?b$+JO0PIveC~OHPtesv zOw%+kFRzKS-Y`htxiaQpQqDLq>ZuHmx!126gm<#kK~rCk8sp>rZJjo?_GDw*{A15t zKblrQkFGz6dd@vB*W^CZRaaMkf3dVE#Y6w@Yw<4}Z*NzRd@{&6DSy_=5kVt&{o$+f zl%qad=Ir@f(_MpbY)13tx-y@zF>2-?eg)!%I~VQGSyiw-@4~3oiXHydOH8KKNe6AE z^fi6>`)A+UK2tb0GBR?E_o~@b&u5D?A-i*?)mX}cH-2&PV&FISz=*jCNl8p&!W&bb zoS<7zCSEy`^=5v4{^^#R>2(_w#v3>5O17z&ubw<}bIc3*AJi>fxctP8aS?#@|nXi4>=t#u=FeIKx)xD|^Yf^6 zM0rx~oyselKRfNG@(Fs*`tEUeo+vc7BU$Xk2B{k2r literal 0 HcmV?d00001 diff --git a/op_align.py b/op_align.py new file mode 100644 index 0000000..6b193a5 --- /dev/null +++ b/op_align.py @@ -0,0 +1,131 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_align" + bl_label = "Align" + bl_description = "Align vertices, edges or shells" + bl_options = {'REGISTER', 'UNDO'} + + direction : bpy.props.StringProperty(name="Direction", default="top") + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False #self.report({'WARNING'}, "Object must have more than one UV map") + + # Not in Synced mode + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + + return True + + + def execute(self, context): + + align(context, self.direction) + return {'FINISHED'} + + + + + +def align(context, direction): + #Store selection + utilities_uv.selection_store() + + if bpy.context.tool_settings.transform_pivot_point != 'CURSOR': + bpy.context.tool_settings.transform_pivot_point = 'CURSOR' + + #B-Mesh + obj = bpy.context.active_object + bm = bmesh.from_edit_mesh(obj.data); + uv_layers = bm.loops.layers.uv.verify(); + + if len(obj.data.uv_layers) == 0: + print("There is no UV channel or UV data set") + return + + # Collect BBox sizes + boundsAll = utilities_uv.getSelectionBBox() + + mode = bpy.context.scene.tool_settings.uv_select_mode + if mode == 'FACE' or mode == 'ISLAND': + print("____ Align Islands") + + #Collect UV islands + islands = utilities_uv.getSelectionIslands() + + for island in islands: + + bpy.ops.uv.select_all(action='DESELECT') + utilities_uv.set_selected_faces(island) + bounds = utilities_uv.getSelectionBBox() + + # print("Island "+str(len(island))+"x faces, delta: "+str(delta.y)) + + if direction == "bottom": + delta = boundsAll['min'] - bounds['min'] + bpy.ops.transform.translate(value=(0, delta.y, 0)) + elif direction == "top": + delta = boundsAll['max'] - bounds['max'] + bpy.ops.transform.translate(value=(0, delta.y, 0)) + elif direction == "left": + delta = boundsAll['min'] - bounds['min'] + bpy.ops.transform.translate(value=(delta.x, 0, 0)) + elif direction == "right": + delta = boundsAll['max'] - bounds['max'] + bpy.ops.transform.translate(value=(delta.x, 0, 0)) + else: + print("Unkown direction: "+str(direction)) + + + elif mode == 'EDGE' or mode == 'VERTEX': + print("____ Align Verts") + + for f in bm.faces: + if f.select: + for l in f.loops: + luv = l[uv_layers] + if luv.select: + # print("Idx: "+str(luv.uv)) + if direction == "top": + luv.uv[1] = boundsAll['max'].y + elif direction == "bottom": + luv.uv[1] = boundsAll['min'].y + elif direction == "left": + luv.uv[0] = boundsAll['min'].x + elif direction == "right": + luv.uv[0] = boundsAll['max'].x + + + bmesh.update_edit_mesh(obj.data) + + #Restore selection + utilities_uv.selection_restore() + +bpy.utils.register_class(op) + + + + + diff --git a/op_align_bottom.png b/op_align_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..e1a05360e94e601eb42dc41c5d655871f9eda533 GIT binary patch literal 604 zcmV-i0;BzjP)cTe@Py20uS&4p8+q&oQyU8 z|Ksyp2`48D0v*H*=p6GgR*6KVOyX+ZNSQ&AN>vh4Ze1p7m_N-3C3MCrk&rUSEJ(x{ zYfJn{LatFUaD(~`SI)=Z-HdBdAt8wyK>&ooj^WDf@b>`YI#fu=3Ij#e^RH%CPR1$H zuR@|$R%(_sEwNN&RE0znh+340f^iiR-=20u;-D>IOV|>&#J`o0mvPIY#3mTCC=un8 zVEF;lElMndVT%%L>_aR+1*^=&GJ$*jo(p7m23LUd%!ac(i*NE!_C1w)&oV)_mfKD*YF6snak_&Lm(n=rE z23ol#5sqD^aSGzBLN~X=!)lj;LXe}er!3>D2Eim);C^o~$sXCIm`E&wSx&G7)_DW= z(uni$hM99lwi=AEEp)R0T7j(I@ApTN$>dEUk+{eAqe33QT__Yf;%#yK842Td1bH4* zk#ReW2U8ACl|sH8%y$`HY>S#&NF@LO04;PySaefwW^{L9a%BKwc`jmXZ*OE|c`jpe qd2n=ZE@^FHXJsx>PDe5{MQ&qnWMy)w27m4V0000ynj$0~E+H-r zHMa)+1rkjat$~YuPkf6gE8p#TB}n+d@d)pH-n-|X>oW|)Xao+>LLAKL!f*l)@DmSn z+63HSG!ls%3KBD?O`r=b=kxiZATe{=1bko<#qR`(K`;BElgknUVG9iKy6!4^>ZRCr zp-^}fBnCq)yaIMX7UZhL1P34vg21IBFe$~GJafgd=vldaW?`<<8G< z%4K0NacLQmz%|lb4$A`4h{rYvnCu_R0;{r~HVEX{l>Y)sJ&?6Q;6$|t7C_3TT@X+a zm;%w^*`0fp86dRvC7y7Nb=5f#qi1Uo7h7Aw{OJD|! ze>f*(%=f%ICn^erXdmZXH!VDYpU6-&Oo@sDVjfSOUYDc^Rr}yODB=4`U{TibH@l#U z9g^X4R1}b7*sQU@>yWC=B>eLNbzg$IH{shhjO}05WgFIg5c&C|kN@eyHu7l{%a<(_ ze-I>QPTPx#7p$T9tspUT+5|elbTXN|5F}v$`1GNDO=WiC)oM=~@; ZZewp`Wpbznf9?PP002ovPDHLkV1ik%_8X}sO7NafY`wtG&e2bq%2N2NJ3hYhB)lBB+U*ElB5x7?Vv=IwdgmOiZnxa5lubT|Z`b1U!y zH)sL#2>wRc2925o=O1Q30vtNTKG*^apdSRml92!nXWafBJs*rNC-r83*A-&SftZm1 zmp#EBx2@bd&iRx64Pqq0wvhmr|4@Oz4dWD2;2xa(3lvh|+H4P~It6=Xry#F+pscR{ zquC!gLyTFoJdm$U$Qj3F+@MrH!%X;7EeL=fFbpPSu-`HXMnJEO+X*5@0zXx-Tvvzn zU>%HuK@hbnkn{fF285#xJII=3hJ6+r%fg`YM=NL{^`8Gf89>ey&h(0vE_`oCtHtZxKK87h9i70a^W#9qH zh^tlwGHKdtv%0_5^AMSnLuVwRS0Yse*wylencocsek|g04;PySaefwW^{L9a%BKwc`jmXZ*OE| uc`jped2n=ZE@^FHXJsx>PDe5{MQ&qnWMy)w27m4V0000J%=P)5DAA8F$W2XXmF5_AS1+~p+Ur$L8~jYkGKnv zr_x)+8)73)C1Q3-4T)90@lGUE&h1LL60U?R;Y$3e#4e~fkf`wNAw+@(mjeB_f-9zo zl1T6~1%6w#g9`6aj4Vnb&dCE@?pOz5-jjKz6e3}E*?zpxUxpvzr7H#L4AA6BlKEg%=rT_o{Ep$a# zbW?9;ba!ELWdLG%E@EtNZ)9Y7E@N_eaCC1jX>DO=WiC)oM=~@;Zewp`Wpbznf9?PP N002ovPDHLkV1mjA{H6c^ literal 0 HcmV?d00001 diff --git a/op_bake.png b/op_bake.png new file mode 100644 index 0000000000000000000000000000000000000000..5a7ee896037c1d01408cf1fe3423df585441d95d GIT binary patch literal 16663 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 M$&tZdy)|pmACVXaEC2ui literal 0 HcmV?d00001 diff --git a/op_bake.py b/op_bake.py new file mode 100644 index 0000000..18c19eb --- /dev/null +++ b/op_bake.py @@ -0,0 +1,581 @@ +import bpy +import os +import bmesh +from mathutils import Vector +from collections import defaultdict +from math import pi +from random import random + +from . import utilities_ui +from . import settings +from . import utilities_bake as ub #Use shorthand ub = utitlites_bake + + +# Notes: https://docs.blender.org/manual/en/dev/render/blender_render/bake.html +modes={ + 'normal_tangent': ub.BakeMode('', type='NORMAL', color=(0.5, 0.5, 1, 1), use_project=True), + 'normal_object': ub.BakeMode('', type='NORMAL', color=(0.5, 0.5, 1, 1), normal_space='OBJECT' ), + 'cavity': ub.BakeMode('bake_cavity', type='EMIT', setVColor=ub.setup_vertex_color_dirty), + 'paint_base': ub.BakeMode('bake_paint_base', type='EMIT'), + 'dust': ub.BakeMode('bake_dust', type='EMIT', setVColor=ub.setup_vertex_color_dirty), + 'id_element': ub.BakeMode('bake_vertex_color',type='EMIT', setVColor=ub.setup_vertex_color_id_element), + 'id_material': ub.BakeMode('bake_vertex_color',type='EMIT', setVColor=ub.setup_vertex_color_id_material), + 'selection': ub.BakeMode('bake_vertex_color',type='EMIT', color=(0, 0, 0, 1), setVColor=ub.setup_vertex_color_selection), + 'diffuse': ub.BakeMode('', type='DIFFUSE'), + # 'displacment': ub.BakeMode('', type='DISPLACEMENT', use_project=True, color=(0, 0, 0, 1), engine='CYCLES'), + 'ao': ub.BakeMode('', type='AO', params=["bake_samples"], engine='CYCLES'), + 'ao_legacy': ub.BakeMode('', type='AO', params=["bake_samples"], engine='CYCLES'), + 'position': ub.BakeMode('bake_position', type='EMIT'), + 'curvature': ub.BakeMode('', type='NORMAL', use_project=True, params=["bake_curvature_size"], composite="curvature"), + 'wireframe': ub.BakeMode('bake_wireframe', type='EMIT', color=(0, 0, 0, 1), params=["bake_wireframe_size"]) +} + +if hasattr(bpy.types,"ShaderNodeBevel"): + # Has newer bevel shader (2.7 nightly build series) + modes['bevel_mask'] = ub.BakeMode('bake_bevel_mask', type='EMIT', color=(0, 0, 0, 1), params=["bake_bevel_samples","bake_bevel_size"]) + modes['normal_tangent_bevel'] = ub.BakeMode('bake_bevel_normal', type='NORMAL', color=(0.5, 0.5, 1, 1), params=["bake_bevel_samples","bake_bevel_size"]) + modes['normal_object_bevel'] = ub.BakeMode('bake_bevel_normal', type='NORMAL', color=(0.5, 0.5, 1, 1), normal_space='OBJECT', params=["bake_bevel_samples","bake_bevel_size"]) + + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_bake" + bl_label = "Bake" + bl_description = "Bake selected objects" + + @classmethod + def poll(cls, context): + if len(settings.sets) == 0: + return False + return True + + def execute(self, context): + bake_mode = utilities_ui.get_bake_mode() + + if bake_mode not in modes: + self.report({'ERROR_INVALID_INPUT'}, "Uknown mode '{}' only available: '{}'".format(bake_mode, ", ".join(modes.keys() )) ) + return + + # Store Selection + selected_objects = [obj for obj in bpy.context.selected_objects] + active_object = bpy.context.view_layer.objects.active + ub.store_bake_settings() + + # Render sets + bake( + self = self, + mode = bake_mode, + size = bpy.context.scene.texToolsSettings.size, + + bake_single = bpy.context.scene.texToolsSettings.bake_force_single, + sampling_scale = int(bpy.context.scene.texToolsSettings.bake_sampling), + samples = bpy.context.scene.texToolsSettings.bake_samples, + ray_distance = bpy.context.scene.texToolsSettings.bake_ray_distance + ) + + # Restore selection + ub.restore_bake_settings() + bpy.ops.object.select_all(action='DESELECT') + for obj in selected_objects: + obj.select_set( state = True, view_layer = None) + if active_object: + bpy.context.view_layer.objects.active = active_object + + return {'FINISHED'} + + + +def bake(self, mode, size, bake_single, sampling_scale, samples, ray_distance): + + print("Bake '{}'".format(mode)) + + bpy.context.scene.render.engine = modes[mode].engine #Switch render engine + + # Disable edit mode + if bpy.context.view_layer.objects.active != None and bpy.context.object.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + ub.store_materials_clear() + + # Get the baking sets / pairs + sets = settings.sets + + render_width = sampling_scale * size[0] + render_height = sampling_scale * size[1] + + for s in range(0,len(sets)): + set = sets[s] + + # Get image name + name_texture = "{}_{}".format(set.name, mode) + if bake_single: + name_texture = "{}_{}".format(sets[0].name, mode)# In Single mode bake into same texture + path = bpy.path.abspath("//{}.tga".format(name_texture)) + + # Requires 1+ low poly objects + if len(set.objects_low) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No low poly object as part of the '{}' set".format(set.name) ) + return + + # Check for UV maps + for obj in set.objects_low: + if not obj.data.uv_layers or len(obj.data.uv_layers) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No UV map available for '{}'".format(obj.name)) + return + + # Check for cage inconsistencies + if len(set.objects_cage) > 0 and (len(set.objects_low) != len(set.objects_cage)): + self.report({'ERROR_INVALID_INPUT'}, "{}x cage objects do not match {}x low poly objects for '{}'".format(len(set.objects_cage), len(set.objects_low), obj.name)) + return + + # Get Materials + material_loaded = get_material(mode) + material_empty = None + if "TT_bake_node" in bpy.data.materials: + material_empty = bpy.data.materials["TT_bake_node"] + else: + material_empty = bpy.data.materials.new(name="TT_bake_node") + + + # Assign Materials to Objects + if (len(set.objects_high) + len(set.objects_float)) == 0: + # Low poly bake: Assign material to lowpoly + for obj in set.objects_low: + assign_vertex_color(mode, obj) + assign_material(mode, obj, material_loaded, material_empty) + else: + # High to low poly: Low poly require empty material to bake into image + for obj in set.objects_low: + assign_material(mode, obj, None, material_empty) + + # Assign material to highpoly + for obj in (set.objects_high+set.objects_float): + assign_vertex_color(mode, obj) + assign_material(mode, obj, material_loaded) + + + # Setup Image + is_clear = (not bake_single) or (bake_single and s==0) + image = setup_image(mode, name_texture, render_width, render_height, path, is_clear) + + # Assign bake node to Material + setup_image_bake_node(set.objects_low[0], image) + + + print("Bake '{}' = {}".format(set.name, path)) + + # Hide all cage objects i nrender + for obj_cage in set.objects_cage: + obj_cage.hide_render = True + + # Bake each low poly object in this set + for i in range(len(set.objects_low)): + obj_low = set.objects_low[i] + obj_cage = None if i >= len(set.objects_cage) else set.objects_cage[i] + + # Disable hide render + obj_low.hide_render = False + + bpy.ops.object.select_all(action='DESELECT') + obj_low.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj_low + + if modes[mode].engine == 'BLENDER_EEVEE': + # Assign image to texture faces + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + area.spaces[0].image = image + # bpy.data.screens['UV Editing'].areas[1].spaces[0].image = image + + + bpy.ops.object.mode_set(mode='OBJECT') + + for obj_high in (set.objects_high): + obj_high.select_set( state = True, view_layer = None) + cycles_bake( + mode, + bpy.context.scene.texToolsSettings.padding, + sampling_scale, + samples, + ray_distance, + len(set.objects_high) > 0, + obj_cage + ) + + # Bake Floaters seperate bake + if len(set.objects_float) > 0: + bpy.ops.object.select_all(action='DESELECT') + for obj_high in (set.objects_float): + obj_high.select_set( state = True, view_layer = None) + obj_low.select_set( state = True, view_layer = None) + + cycles_bake( + mode, + 0, + sampling_scale, + samples, + ray_distance, + len(set.objects_float) > 0, + obj_cage + ) + + # Set background image (CYCLES & BLENDER_EEVEE) + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + area.spaces[0].image = image + + # Restore renderable for cage objects + for obj_cage in set.objects_cage: + obj_cage.hide_render = False + + + # Downsample image? + if not bake_single or (bake_single and s == len(sets)-1): + # When baking single, only downsample on last bake + if render_width != size[0] or render_height != size[1]: + image.scale(size[0],size[1]) + + # Apply composite nodes on final image result + if modes[mode].composite: + apply_composite(image, modes[mode].composite, bpy.context.scene.texToolsSettings.bake_curvature_size) + + # image.save() + + # Restore non node materials + ub.restore_materials() + + + + +def apply_composite(image, scene_name, size): + previous_scene = bpy.context.window.scene + + # Get Scene with compositing nodes + scene = None + if scene_name in bpy.data.scenes: + scene = bpy.data.scenes[scene_name] + else: + path = os.path.join(os.path.dirname(__file__), "resources/compositing.blend")+"\\Scene\\" + bpy.ops.wm.append(filename=scene_name, directory=path, link=False, autoselect=False) + scene = bpy.data.scenes[scene_name] + + if scene: + # Switch scene + bpy.context.window.scene = scene + + #Setup composite nodes for Curvature + if "Image" in scene.node_tree.nodes: + scene.node_tree.nodes["Image"].image = image + + if "Offset" in scene.node_tree.nodes: + scene.node_tree.nodes["Offset"].outputs[0].default_value = size + print("Assign offset: {}".format(scene.node_tree.nodes["Offset"].outputs[0].default_value)) + + # Render image + bpy.ops.render.render(use_viewport=False) + + + # Get last images of viewer node and render result + image_viewer_node = get_last_item("Viewer Node", bpy.data.images) + image_render_result = get_last_item("Render Result", bpy.data.images) + + #Copy pixels + image.pixels = image_viewer_node.pixels[:] + image.update() + + if image_viewer_node: + bpy.data.images.remove(image_viewer_node) + if image_render_result: + bpy.data.images.remove(image_render_result) + + #Restore scene & remove other scene + bpy.context.window.scene = previous_scene + + # Delete compositing scene + bpy.data.scenes.remove(scene) + + + +def get_last_item(key_name, collection): + # bpy.data.images + # Get last image of a series, e.g. .001, .002, 003 + keys = [] + for item in collection: + if key_name in item.name: + keys.append(item.name) + + print("Search for {}x : '{}'".format(len(keys), ",".join(keys) ) ) + + if len(keys) > 0: + return collection[keys[-1]] + + return None + + + + +def setup_image(mode, name, width, height, path, is_clear): + image = None + + print("Path "+path) + if name in bpy.data.images: + image = bpy.data.images[name] + if image.source == 'FILE': + # Clear image if it was deleted outside + if not os.path.isfile(image.filepath): + image.user_clear() + bpy.data.images.remove(image) + # bpy.data.images[name].update() + + # if bpy.data.images[name].has_data == False: + + + # Previous image does not have data, remove first + # print("Image pointer exists but no data "+name) + # image = bpy.data.images[name] + # image.update() + # image.generated_height = height + # bpy.data.images.remove(bpy.data.images[name]) + + if name not in bpy.data.images: + # Create new image with 32 bit float + is_float_32 = bpy.context.preferences.addons["textools"].preferences.bake_32bit_float == '32' + image = bpy.data.images.new(name, width=width, height=height, float_buffer=is_float_32) + if "_normal_" in image.name: + image.colorspace_settings.name = 'Non-Color' + else: + image.colorspace_settings.name = 'sRGB' + + + else: + # Reuse existing Image + image = bpy.data.images[name] + # Reisze? + if image.size[0] != width or image.size[1] != height or image.generated_width != width or image.generated_height != height: + image.generated_width = width + image.generated_height = height + image.scale(width, height) + + # Fill with plain color + if is_clear: + image.generated_color = modes[mode].color + image.generated_type = 'BLANK' + + + image.file_format = 'TARGA' + + # TODO: Verify that the path exists + # image.filepath_raw = path + + return image + + + +def setup_image_bake_node(obj, image): + + if len(obj.data.materials) <= 0: + print("ERROR, need spare material to setup active image texture to bake!!!") + else: + for slot in obj.material_slots: + if slot.material: + if(slot.material.use_nodes == False): + slot.material.use_nodes = True + + # Assign bake node + tree = slot.material.node_tree + node = None + if "bake" in tree.nodes: + node = tree.nodes["bake"] + else: + node = tree.nodes.new("ShaderNodeTexImage") + node.name = "bake" + node.select = True + node.image = image + tree.nodes.active = node + + + +def assign_vertex_color(mode, obj): + if modes[mode].setVColor: + modes[mode].setVColor(obj) + + + +def assign_material(mode, obj, material_bake=None, material_empty=None): + ub.store_materials(obj) + + bpy.context.view_layer.objects.active = obj + obj.select_set( state = True, view_layer = None) + + # Select All faces + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + faces = [face for face in bm.faces if face.select] + bpy.ops.mesh.select_all(action='SELECT') + + + if material_bake: + # Setup properties of bake materials + if mode == 'wireframe': + if "Value" in material_bake.node_tree.nodes: + material_bake.node_tree.nodes["Value"].outputs[0].default_value = bpy.context.scene.texToolsSettings.bake_wireframe_size + if mode == 'bevel_mask': + if "Bevel" in material_bake.node_tree.nodes: + material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size + material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples + if mode == 'normal_tangent_bevel': + if "Bevel" in material_bake.node_tree.nodes: + material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size + material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples + if mode == 'normal_object_bevel': + if "Bevel" in material_bake.node_tree.nodes: + material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size + material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples + + + + # Don't apply in diffuse mode + if mode != 'diffuse': + if material_bake: + # Override with material_bake + if len(obj.material_slots) == 0: + obj.data.materials.append(material_bake) + + else: + obj.material_slots[0].material = material_bake + obj.active_material_index = 0 + bpy.ops.object.material_slot_assign() + + elif material_empty: + #Assign material_empty if no material available + if len(obj.material_slots) == 0: + obj.data.materials.append(material_empty) + + else: # not obj.material_slots[0].material: + obj.material_slots[0].material = material_empty + obj.active_material_index = 0 + bpy.ops.object.material_slot_assign() + + # Restore Face selection + bpy.ops.mesh.select_all(action='DESELECT') + for face in faces: + face.select = True + + bpy.ops.object.mode_set(mode='OBJECT') + + + + + + + +def get_material(mode): + + + + if modes[mode].material == "": + return None # No material setup requires + + # Find or load material + name = modes[mode].material + path = os.path.join(os.path.dirname(__file__), "resources/materials.blend")+"\\Material\\" + if "bevel" in mode: + path = os.path.join(os.path.dirname(__file__), "resources/materials_2.80.blend")+"\\Material\\" + + print("Get mat {}\n{}".format(mode, path)) + + if bpy.data.materials.get(name) is None: + print("Material not yet loaded: "+mode) + bpy.ops.wm.append(filename=name, directory=path, link=False, autoselect=False) + + return bpy.data.materials.get(name) + + + + +def cycles_bake(mode, padding, sampling_scale, samples, ray_distance, is_multi, obj_cage): + + + # if modes[mode].engine == 'BLENDER_EEVEE': + # # Snippet: https://gist.github.com/AndrewRayCode/760c4634a77551827de41ed67585064b + # bpy.context.scene.render.bake_margin = padding + + # # AO Settings + # bpy.context.scene.render.bake_type = modes[mode].type + # bpy.context.scene.render.use_bake_normalize = True + + # if modes[mode].type == 'AO': + # bpy.context.scene.world.light_settings.use_ambient_occlusion = True + # bpy.context.scene.world.light_settings.gather_method = 'RAYTRACE' + # bpy.context.scene.world.light_settings.samples = samples + + # bpy.context.scene.render.use_bake_selected_to_active = is_multi + # bpy.context.scene.render.bake_distance = ray_distance + # bpy.context.scene.render.use_bake_clear = False + + # bpy.ops.object.bake_image() + + + if modes[mode].engine == 'CYCLES' or modes[mode].engine == 'BLENDER_EEVEE' : + + if modes[mode].normal_space == 'OBJECT': + #See: https://twitter.com/Linko_3D/status/963066705584054272 + bpy.context.scene.render.bake.normal_r = 'POS_X' + bpy.context.scene.render.bake.normal_g = 'POS_Z' + bpy.context.scene.render.bake.normal_b = 'NEG_Y' + + elif modes[mode].normal_space == 'TANGENT': + bpy.context.scene.render.bake.normal_r = 'POS_X' + bpy.context.scene.render.bake.normal_b = 'POS_Z' + # Adjust Y swizzle from Addon preferences + swizzle_y = bpy.context.preferences.addons["textools"].preferences.swizzle_y_coordinate + if swizzle_y == 'Y-': + bpy.context.scene.render.bake.normal_g = 'NEG_Y' + elif swizzle_y == 'Y+': + bpy.context.scene.render.bake.normal_g = 'POS_Y' + + # Set samples + bpy.context.scene.cycles.samples = samples + + # Speed up samples for simple render modes + if modes[mode].type == 'EMIT' or modes[mode].type == 'DIFFUSE': + bpy.context.scene.cycles.samples = 1 + + # Pixel Padding + bpy.context.scene.render.bake.margin = padding * sampling_scale + + # Disable Direct and Indirect for all 'DIFFUSE' bake types + if modes[mode].type == 'DIFFUSE': + bpy.context.scene.render.bake.use_pass_direct = False + bpy.context.scene.render.bake.use_pass_indirect = False + bpy.context.scene.render.bake.use_pass_color = True + + if obj_cage is None: + # Bake with Cage + bpy.ops.object.bake( + type=modes[mode].type, + use_clear=False, + cage_extrusion=ray_distance, + + use_selected_to_active=is_multi, + normal_space=modes[mode].normal_space + ) + else: + # Bake without Cage + bpy.ops.object.bake( + type=modes[mode].type, + use_clear=False, + cage_extrusion=ray_distance, + + use_selected_to_active=is_multi, + normal_space=modes[mode].normal_space, + + #Use Cage and assign object + use_cage=True, + cage_object=obj_cage.name + ) + +bpy.utils.register_class(op) diff --git a/op_bake_explode.png b/op_bake_explode.png new file mode 100644 index 0000000000000000000000000000000000000000..2dc23c4c9f9db10dd8d7294a0fb32240a1f85383 GIT binary patch literal 16863 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 0: + for i in range(0,3): + if abs(delta[i]) == delta_max: + direction[i] = delta[i]/abs(delta[i]) + else: + direction[i] = 0 + else: + # Default when not delta offset was measure move up + direction = [0,0,1] + + delta = Vector((direction[0], direction[1], direction[2])) + + # Get Key + key = get_delta_key(delta) + + # Calculate Offset + bbox = get_bbox_set(set) + bbox_last = dir_offset_last_bbox[key] + + offset = Vector((0,0,0)) + + if delta.x == 1: + offset = delta * ( bbox_last['max'].x - bbox['min'].x ) + elif delta.x == -1: + offset = delta * -( bbox_last['min'].x - bbox['max'].x ) + + elif delta.y == 1: + offset = delta * ( bbox_last['max'].y - bbox['min'].y ) + elif delta.y == -1: + offset = delta * -( bbox_last['min'].y - bbox['max'].y ) + + elif delta.z == 1: + offset = delta * ( bbox_last['max'].z - bbox['min'].z ) + elif delta.z == -1: + offset = delta * -( bbox_last['min'].z - bbox['max'].z ) + + # Add margin + offset+= delta * margin + + # Offset items + # https://blenderartists.org/forum/showthread.php?237761-Blender-2-6-Set-keyframes-using-Python-script + # http://blenderscripting.blogspot.com.au/2011/05/inspired-by-post-on-ba-it-just-so.html + + # Set key A + bpy.context.scene.frame_current = 0 + for obj in objects: + obj.keyframe_insert(data_path="location") + + for obj in objects: + obj.location += offset + bpy.context.view_layer.update() + + # Set key B + bpy.context.scene.frame_current = frame_range + for obj in objects: + obj.keyframe_insert(data_path="location") + + # Update last bbox in direction + dir_offset_last_bbox[key] = get_bbox_set(set) + + + + +def get_delta_key(delta): + # print("Get key {} is: {}".format(delta, delta.y == -1 )) + if delta.x == -1: + return 0 + elif delta.x == 1: + return 1 + if delta.y == -1: + return 2 + elif delta.y == 1: + return 3 + if delta.z == -1: + return 4 + elif delta.z == 1: + return 5 + + + +def merge_bounds(bounds): + box_min = bounds[0]['min'].copy() + box_max = bounds[0]['max'].copy() + + for bbox in bounds: + # box_min.x = -8 + box_min.x = min(box_min.x, bbox['min'].x) + box_min.y = min(box_min.y, bbox['min'].y) + box_min.z = min(box_min.z, bbox['min'].z) + + box_max.x = max(box_max.x, bbox['max'].x) + box_max.y = max(box_max.y, bbox['max'].y) + box_max.z = max(box_max.z, bbox['max'].z) + + return { + 'min':box_min, + 'max':box_max, + 'size':(box_max-box_min), + 'center':box_min+(box_max-box_min)/2 + } + + + +def get_bbox_set(set): + objects = set.objects_low + set.objects_high + set.objects_cage + bounds = [] + for obj in objects: + bounds.append( get_bbox(obj) ) + return merge_bounds(bounds) + + + +def get_bbox(obj): + corners = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box] + + # Get world space Min / Max + box_min = Vector((corners[0].x, corners[0].y, corners[0].z)) + box_max = Vector((corners[0].x, corners[0].y, corners[0].z)) + for corner in corners: + # box_min.x = -8 + box_min.x = min(box_min.x, corner.x) + box_min.y = min(box_min.y, corner.y) + box_min.z = min(box_min.z, corner.z) + + box_max.x = max(box_max.x, corner.x) + box_max.y = max(box_max.y, corner.y) + box_max.z = max(box_max.z, corner.z) + + return { + 'min':box_min, + 'max':box_max, + 'size':(box_max-box_min), + 'center':box_min+(box_max-box_min)/2 + } + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_bake_organize_names.py b/op_bake_organize_names.py new file mode 100644 index 0000000..0da6320 --- /dev/null +++ b/op_bake_organize_names.py @@ -0,0 +1,179 @@ +import bpy +import bmesh +import operator +import math +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_bake + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_bake_organize_names" + bl_label = "Match Names" + bl_description = "Match high poly object names to low poly objects by their bounding boxes." + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # Require 2 or more objects to sort + if len(bpy.context.selected_objects) <= 1: + return False + + return True + + + def execute(self, context): + sort_objects(self) + return {'FINISHED'} + + + +def sort_objects(self): + # Collect objects + objects = [] + bounds = {} + for obj in bpy.context.selected_objects: + if obj.type == 'MESH': + objects.append(obj) + bounds[obj] = get_bbox(obj) + + print("Objects {}x".format(len(objects))) + + # Get smallest side of any bounding box + min_side = min(bounds[objects[0]]['size'].x, bounds[objects[0]]['size'].y, bounds[objects[0]]['size'].z) + avg_side = 0 + for obj in bounds: + min_side = min(min_side, bounds[obj]['size'].x, bounds[obj]['size'].y, bounds[obj]['size'].z) + avg_side+=bounds[obj]['size'].x + avg_side+=bounds[obj]['size'].y + avg_side+=bounds[obj]['size'].z + avg_side/=(len(bounds)*3) + + # Get all Low and high poly objects + objects_low = [obj for obj in objects if utilities_bake.get_object_type(obj)=='low'] + objects_high = [obj for obj in objects if utilities_bake.get_object_type(obj)=='high'] + + if len(objects_low) == 0: + self.report({'ERROR_INVALID_INPUT'}, "There are no low poly objects selected") + return + elif len(objects_high) == 0: + self.report({'ERROR_INVALID_INPUT'}, "There are no high poly objects selected") + return + + print("Low {}x, High {}x".format(len(objects_low),len(objects_high))) + + pairs_low_high = {} + + objects_left_high = objects_high.copy() + for obj_A in objects_low: + + matches = {} + for obj_B in objects_left_high: + score = get_score(obj_A, obj_B) + p = score / avg_side + if p > 0 and p <= 0.65: + matches[obj_B] = p + else: + print("Not matched: {} ".format(p)) + + if(len(matches) > 0): + sorted_matches = sorted(matches.items(), key=operator.itemgetter(1)) + for i in range(0, len(sorted_matches)): + A = obj_A + B = sorted_matches[i][0] + p = sorted_matches[i][1] + print("Check: {}% '{}' = '{}' ".format(int(p * 100.0), A.name, B.name )) + + # Remove from list + objects_left_high.remove(sorted_matches[0][0]) + pairs_low_high[obj_A] = sorted_matches[0][0] + print("") + + # objects_unsorted = [obj for obj in objects if (obj not in pairs_low_high.values() and obj not in pairs_low_high.keys() )] + + bpy.ops.object.select_all(action='DESELECT') + for obj_A in pairs_low_high: + obj_B = pairs_low_high[obj_A] + try: + obj_B.name = utilities_bake.get_bake_name(obj_A)+" high" + + obj_A.select = True + obj_B.select = True + except: + print("Fallo") + + print("Matched {}x".format(len(pairs_low_high))) + + + +def get_score(A, B): + + bbox_A = get_bbox(A) + bbox_B = get_bbox(B) + + # Not colliding + if not is_colliding(bbox_A, bbox_B): + return -1.0 + + # Position + delta_pos = (bbox_B['center'] - bbox_A['center']).length + + # Volume + volume_A = bbox_A['size'].x * bbox_A['size'].y * bbox_A['size'].z + volume_B = bbox_B['size'].x * bbox_B['size'].y * bbox_B['size'].z + delta_vol = (max(volume_A, volume_B) - min(volume_A, volume_B))/3.0 + + # Longest side + side_A_max = max(bbox_A['size'].x, bbox_A['size'].y, bbox_A['size'].z ) + side_B_max = max(bbox_B['size'].x, bbox_B['size'].y, bbox_B['size'].z ) + delta_size_max = abs(side_A_max - side_B_max) + + return delta_pos + delta_vol + delta_size_max + + + +def get_bbox(obj): + corners = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box] + + # Get world space Min / Max + box_min = Vector((corners[0].x, corners[0].y, corners[0].z)) + box_max = Vector((corners[0].x, corners[0].y, corners[0].z)) + for corner in corners: + # box_min.x = -8 + box_min.x = min(box_min.x, corner.x) + box_min.y = min(box_min.y, corner.y) + box_min.z = min(box_min.z, corner.z) + + box_max.x = max(box_max.x, corner.x) + box_max.y = max(box_max.y, corner.y) + box_max.z = max(box_max.z, corner.z) + + return { + 'min':box_min, + 'max':box_max, + 'size':(box_max-box_min), + 'center':box_min+(box_max-box_min)/2 + } + + + +def is_colliding(bbox_A, bbox_B): + def is_collide_1D(A_min, A_max, B_min, B_max): + # One line is inside the other + length_A = A_max-A_min + length_B = B_max-B_min + center_A = A_min + length_A/2 + center_B = B_min + length_B/2 + + return abs(center_A - center_B) <= (length_A+length_B)/2 + + collide_x = is_collide_1D(bbox_A['min'].x, bbox_A['max'].x, bbox_B['min'].x, bbox_B['max'].x) + collide_y = is_collide_1D(bbox_A['min'].y, bbox_A['max'].y, bbox_B['min'].y, bbox_B['max'].y) + collide_z = is_collide_1D(bbox_A['min'].z, bbox_A['max'].z, bbox_B['min'].z, bbox_B['max'].z) + + return collide_x and collide_y and collide_z + +bpy.utils.register_class(op) + diff --git a/op_color_assign.py b/op_color_assign.py new file mode 100644 index 0000000..55635b1 --- /dev/null +++ b/op_color_assign.py @@ -0,0 +1,98 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_color + +class op(bpy.types.Operator): + bl_idname = "uv.textools_color_assign" + bl_label = "Assign Color" + bl_description = "Assign color to selected objects or faces in edit mode." + bl_options = {'REGISTER', 'UNDO'} + + index : bpy.props.IntProperty(description="Color Index", default=0) + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object not in bpy.context.selected_objects: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + def execute(self, context): + assign_color(self, context, self.index) + return {'FINISHED'} + + + +def assign_color(self, context, index): + + selected_obj = bpy.context.selected_objects.copy() + + previous_mode = 'OBJECT' + if len(selected_obj) == 1: + previous_mode = bpy.context.active_object.mode + + + for obj in selected_obj: + # Select object + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + + # Enter Edit mode + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data); + faces = [] + + #Assign to all or just selected faces? + if previous_mode == 'EDIT': + faces = [face for face in bm.faces if face.select] + else: + faces = [face for face in bm.faces] + + if previous_mode == 'OBJECT': + bpy.ops.mesh.select_all(action='SELECT') + + + # Verify material slots + for i in range(index+1): + if index >= len(obj.material_slots): + bpy.ops.object.material_slot_add() + + utilities_color.assign_slot(obj, index) + + # Assign to selection + obj.active_material_index = index + bpy.ops.object.material_slot_assign() + + + #Change View mode to MATERIAL + # for area in bpy.context.screen.areas: + # if area.type == 'VIEW_3D': + # for space in area.spaces: + # if space.type == 'VIEW_3D': + # space.shading.type = 'MATERIAL' + + # restore mode + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + for obj in selected_obj: + obj.select_set( state = True, view_layer = None) + bpy.ops.object.mode_set(mode=previous_mode) + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_color_clear.py b/op_color_clear.py new file mode 100644 index 0000000..dca9d6a --- /dev/null +++ b/op_color_clear.py @@ -0,0 +1,96 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_color + +class op(bpy.types.Operator): + bl_idname = "uv.textools_color_clear" + bl_label = "Clear Colors" + bl_description = "Clears the Color IDs and materials on the selected model" + bl_options = {'REGISTER', 'UNDO'} + + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object not in bpy.context.selected_objects: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + def execute(self, context): + clear_colors(self, context) + return {'FINISHED'} + + + +def clear_colors(self, context): + obj = bpy.context.active_object + + + + # Store previous mode + previous_mode = bpy.context.active_object.mode + if bpy.context.active_object.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + + # Set all faces + for face in bm.faces: + face.material_index = 0 + + # Clear material slots + bpy.ops.object.mode_set(mode='OBJECT') + count = len(obj.material_slots) + for i in range(count): + bpy.ops.object.material_slot_remove() + + # Delete materials if not used + for material in bpy.data.materials: + if utilities_color.material_prefix in material.name: + if material.users == 0: + material.user_clear() + bpy.data.materials.remove(material) + + # Restore previous mode + bpy.ops.object.mode_set(mode=previous_mode) + + + for area in bpy.context.screen.areas: + print("area: {}".format(area.type)) + if area.type == 'PROPERTIES': + for space in area.spaces: + if space.type == 'PROPERTIES': + # space.shading.type = 'MATERIAL' + space.context = 'MATERIAL' + + # Show Material Tab + for area in bpy.context.screen.areas: + if area.type == 'PROPERTIES': + for space in area.spaces: + if space.type == 'PROPERTIES': + space.context = 'MATERIAL' + + # Switch Solid shading + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + for space in area.spaces: + if space.type == 'VIEW_3D': + space.shading.type = 'SOLID' + + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_color_convert_texture.png b/op_color_convert_texture.png new file mode 100644 index 0000000000000000000000000000000000000000..808d92c8a9e5c194517b9339443b38e90dd94852 GIT binary patch literal 15076 zcmeI3&yU+g6vt;-sn9KL1sqrbm5eL#+wreBiLE5nlC<5G$gz1S&kT6~gLwe- z+jf0DT(7-mm~J}*Ew_O(y|#yE15iBG^PqVNg8Hb1 ziQfuC&k)6Kx0~sz8Q0$wWnI@rNf8xAz%ztk+X-P$aDtObkU^X(3QXVjLfdt?C@yTc zov_67QKI3|zpl17OymT8c9^2rgPtg7Byp0HWe$D3j^Bz;%`!#QLT%)P0rrz8`gvP! z=muNvl$1kxYGS-5wc0TE#Pe;pC#DX<)0Z)VKGKPvL4Dgp;wlQzDSWW_1sa$qE(mwHg&4!O->VuG=anRkkuf$W*Lcx zWwj_y8cun|m1ZnCK?ofaEmg~SAYwxiu*=Kr#1BKyV>K*c`yV?{FM9j9NjJC(Ap~r70;R;BygKy7y4*n6IaMVIzNfJ z-;E4k6r+1V%l;1*B7m3w-7*}ktNz(COx&UVzhxLzvAG4EO=OkDK>-g24z_a1dhYM6 zLAQ=~Vllco8MupaMZvgxHp-mLUyr)6)rsfl6$u~s5e6T9*R)K!ZqQxRGU+<< ze8aKBveM5+45z%JJ7}`Kui=M1RL*Idsue|YnDkh&d#JTuwedb2MEi+~GsQlnFwt>i znqx}nJ0?q&dMt~3Fd4-UefW7(96oT4wdZgbdys!dSM(sG60ukk0tnq?xafQcmEj_Q z&`pMm&WBJLE&>SMWVq;j2$kU?fY42bi_V8o87=|{-DJ4vdSMWVq;j2$kU? zfY42bi_V8o87=|{-DJ4vdSMWVq;j2$kU?fY41UuDQ`GoXElNZFcbsn!?6! z(L14>xwu*b;GL%dc<(-#nLP`@Th9aV_AUUw-U1;1App;wY24{x8~97rg?jI&M*aNx zzkZZ18sk3C+0@{5-$E4R)azjx!Yhdwzr-}(Of?4h4$ z_P+dh{j)!wTe!KlcTbr={4IZd`R1oEJ9p-U^vmwKhabIh%^Mnf#>J0tHgNy`EI9VV V@!r?wwWY{jI=x)|{MCzB{sy_MOW*(i literal 0 HcmV?d00001 diff --git a/op_color_convert_texture.py b/op_color_convert_texture.py new file mode 100644 index 0000000..aac12e9 --- /dev/null +++ b/op_color_convert_texture.py @@ -0,0 +1,153 @@ +import bpy +import bmesh +import operator +import math +from mathutils import Vector +from collections import defaultdict + +from . import utilities_color +from . import utilities_bake +from . import utilities_ui + +material_prefix = "TT_atlas_" +gamma = 2.2 + +class op(bpy.types.Operator): + bl_idname = "uv.textools_color_convert_to_texture" + bl_label = "Pack Texture" + bl_description = "Pack ID Colors into single texture and UVs" + bl_options = {'REGISTER', 'UNDO'} + + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object not in bpy.context.selected_objects: + return False + + if len(bpy.context.selected_objects) != 1: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + def execute(self, context): + pack_texture(self, context) + return {'FINISHED'} + + + +def pack_texture(self, context): + obj = bpy.context.active_object + name = material_prefix+obj.name + + if obj.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + + # Determine size + size_pixel = 8 + size_square = math.ceil(math.sqrt( context.scene.texToolsSettings.color_ID_count )) + size_image = size_square * size_pixel + size_image_pow = int(math.pow(2, math.ceil(math.log(size_image, 2)))) + + # Maximize pixel size + size_pixel = math.floor(size_image_pow/size_square) + + print("{0} colors = {1} x {1} = ({2}pix) {3} x {3} | {4} x {4}".format( + context.scene.texToolsSettings.color_ID_count, + size_square, + size_pixel, + size_image, + size_image_pow + )) + + # Create image + image = bpy.data.images.new(name, width=size_image_pow, height=size_image_pow) + pixels = [None] * size_image_pow * size_image_pow + + # Black pixels + for x in range(size_image_pow): + for y in range(size_image_pow): + pixels[(y * size_image_pow) + x] = [0, 0, 0, 1] + + # Pixels + for c in range(context.scene.texToolsSettings.color_ID_count): + x = c % size_square + y = math.floor(c/size_square) + color = utilities_color.get_color(c).copy() + for i in range(3): + color[i] = pow(color[i] , 1.0/gamma) + + for sx in range(size_pixel): + for sy in range(size_pixel): + _x = x*size_pixel + sx + _y = y*size_pixel + sy + pixels[(_y * size_image_pow) + _x] = [color[0], color[1], color[2], 1] + + + # flatten list & assign pixels + pixels = [chan for px in pixels for chan in px] + image.pixels = pixels + + # Set background image + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + area.spaces[0].image = image + + # Edit mesh + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + bpy.ops.mesh.select_all(action='SELECT') + # bpy.ops.uv.smart_project(angle_limit=1) + bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.0078) + + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify(); + + for face in bm.faces: + index = face.material_index + + # Get UV coordinates for index + x = index%size_square + y = math.floor(index/size_square) + + x*= (size_pixel / size_image_pow) + y*= (size_pixel / size_image_pow) + x+= size_pixel/size_image_pow/2 + y+= size_pixel/size_image_pow/2 + + for loop in face.loops: + loop[uv_layers].uv = (x, y) + + # Remove Slots & add one + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.uv.textools_color_clear() + bpy.ops.object.material_slot_add() + + #Create material with image + obj.material_slots[0].material = utilities_bake.get_image_material(image) + + #Display UVs + bpy.ops.object.mode_set(mode='EDIT') + + # Switch textured shading + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + for space in area.spaces: + if space.type == 'VIEW_3D': + space.shading.type = 'MATERIAL' + + bpy.ops.ui.textools_popup('INVOKE_DEFAULT', message="Packed texture with {} color IDs".format( context.scene.texToolsSettings.color_ID_count )) + + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_color_convert_vertex_colors.png b/op_color_convert_vertex_colors.png new file mode 100644 index 0000000000000000000000000000000000000000..0d465620bca7f8cdc753cad482cc23a6ed68d753 GIT binary patch literal 15587 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 literal 0 HcmV?d00001 diff --git a/op_color_from_directions.py b/op_color_from_directions.py new file mode 100644 index 0000000..95b2c00 --- /dev/null +++ b/op_color_from_directions.py @@ -0,0 +1,206 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_color + +class op(bpy.types.Operator): + bl_idname = "uv.textools_color_from_directions" + bl_label = "Color Directions" + bl_description = "Assign a color ID to different face directions" + bl_options = {'REGISTER', 'UNDO'} + + directions : bpy.props.EnumProperty(items= + [('2', '2', 'Top & Bottom, Sides'), + ('3', '3', 'Top & Bottom, Left & Right, Front & Back'), + ('4', '4', 'Top, Left & Right, Front & Back, Bottom'), + ('6', '6', 'All sides')], + name = "Directions", + default = '3' + ) + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + + # def draw(self, context): + # layout = self.layout + # layout.prop(self, "directions") + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object not in bpy.context.selected_objects: + return False + + if len(bpy.context.selected_objects) != 1: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + def execute(self, context): + color_elements(self, context) + return {'FINISHED'} + + + +def color_elements(self, context): + obj = bpy.context.active_object + + # Setup Edit & Face mode + if obj.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + + # Collect groups + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + + + face_directions = { + 'top':[], + 'bottom':[], + 'left':[], + 'right':[], + 'front':[], + 'back':[] + } + + + print("Directions {}".format(self.directions)) + + + for face in bm.faces: + print("face {} n: {}".format(face.index, face.normal)) + # Find dominant direction + abs_x = abs(face.normal.x) + abs_y = abs(face.normal.y) + abs_z = abs(face.normal.z) + max_xyz = max(abs_x, abs_y, abs_z) + + if max_xyz == abs_x: + if face.normal.x > 0: + face_directions['right'].append(face.index) + else: + face_directions['left'].append(face.index) + elif max_xyz == abs_y: + if face.normal.y > 0: + face_directions['front'].append(face.index) + else: + face_directions['back'].append(face.index) + elif max_xyz == abs_z: + if face.normal.z > 0: + face_directions['top'].append(face.index) + else: + face_directions['bottom'].append(face.index) + + count = int(self.directions) + bpy.context.scene.texToolsSettings.color_ID_count = count + + groups = [] + # for i in range(count): + # groups.append([]) + + if self.directions == '2': + groups.append(face_directions['top']+face_directions['bottom']) + groups.append(face_directions['left']+face_directions['right']+face_directions['front']+face_directions['back']) + if self.directions == '3': + groups.append(face_directions['top']+face_directions['bottom']) + groups.append(face_directions['left']+face_directions['right']) + groups.append(face_directions['front']+face_directions['back']) + elif self.directions == '4': + groups.append(face_directions['top']) + groups.append(face_directions['left']+face_directions['right']) + groups.append(face_directions['front']+face_directions['back']) + groups.append(face_directions['bottom']) + elif self.directions == '6': + groups.append(face_directions['top']) + groups.append(face_directions['right']) + groups.append(face_directions['left']) + groups.append(face_directions['front']) + groups.append(face_directions['back']) + groups.append(face_directions['bottom']) + + # Assign Groups to colors + index_color = 0 + for group in groups: + # # rebuild bmesh data (e.g. left edit mode previous loop) + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + if hasattr(bm.faces, "ensure_lookup_table"): + bm.faces.ensure_lookup_table() + + # Select group + bpy.ops.mesh.select_all(action='DESELECT') + for index_face in group: + bm.faces[index_face].select = True + + # Assign to selection + bpy.ops.uv.textools_color_assign(index=index_color) + + index_color = (index_color+1) % bpy.context.scene.texToolsSettings.color_ID_count + + bpy.ops.object.mode_set(mode='OBJECT') + utilities_color.validate_face_colors(obj) + ''' + faces_indices_processed = [] + + + for face in bm.faces: + if face.index not in faces_indices_processed: + # Select face & extend + bpy.ops.mesh.select_all(action='DESELECT') + face.select = True + bpy.ops.mesh.select_linked(delimit={'NORMAL'}) + + faces = [f.index for f in bm.faces if (f.select and f.index not in faces_indices_processed)] + for f in faces: + faces_indices_processed.append(f) + groups.append(faces) + + + # Assign color count (caps automatically e.g. max 20) + bpy.context.scene.texToolsSettings.color_ID_count = len(groups) + gamma = 2.2 + + for i in range(bpy.context.scene.texToolsSettings.color_ID_count): + color = utilities_color.get_color_id(i, bpy.context.scene.texToolsSettings.color_ID_count) + # Fix Gamma + color[0] = pow(color[0] , gamma) + color[1] = pow(color[1] , gamma) + color[2] = pow(color[2], gamma) + utilities_color.set_color(i, color) + + # Assign Groups to colors + index_color = 0 + for group in groups: + # rebuild bmesh data (e.g. left edit mode previous loop) + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + if hasattr(bm.faces, "ensure_lookup_table"): + bm.faces.ensure_lookup_table() + + # Select group + bpy.ops.mesh.select_all(action='DESELECT') + for index_face in group: + bm.faces[index_face].select = True + + # Assign to selection + bpy.ops.uv.textools_color_assign(index=index_color) + + index_color = (index_color+1) % bpy.context.scene.texToolsSettings.color_ID_count + + bpy.ops.object.mode_set(mode='OBJECT') + utilities_color.validate_face_colors(obj) + ''' + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_color_from_elements.py b/op_color_from_elements.py new file mode 100644 index 0000000..a713a2d --- /dev/null +++ b/op_color_from_elements.py @@ -0,0 +1,102 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_color + +class op(bpy.types.Operator): + bl_idname = "uv.textools_color_from_elements" + bl_label = "Color Elements" + bl_description = "Assign a color ID to each mesh element" + bl_options = {'REGISTER', 'UNDO'} + + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object not in bpy.context.selected_objects: + return False + + if len(bpy.context.selected_objects) != 1: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + def execute(self, context): + color_elements(self, context) + return {'FINISHED'} + + + +def color_elements(self, context): + obj = bpy.context.active_object + + # Setup Edit & Face mode + if obj.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + + # Collect groups + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + faces_indices_processed = [] + groups = [] + + for face in bm.faces: + if face.index not in faces_indices_processed: + # Select face & extend + bpy.ops.mesh.select_all(action='DESELECT') + face.select = True + bpy.ops.mesh.select_linked(delimit={'NORMAL'}) + + faces = [f.index for f in bm.faces if (f.select and f.index not in faces_indices_processed)] + for f in faces: + faces_indices_processed.append(f) + groups.append(faces) + + + # Assign color count (caps automatically e.g. max 20) + bpy.context.scene.texToolsSettings.color_ID_count = len(groups) + gamma = 2.2 + + for i in range(bpy.context.scene.texToolsSettings.color_ID_count): + color = utilities_color.get_color_id(i, bpy.context.scene.texToolsSettings.color_ID_count) + # Fix Gamma + color[0] = pow(color[0] , gamma) + color[1] = pow(color[1] , gamma) + color[2] = pow(color[2], gamma) + utilities_color.set_color(i, color) + + # Assign Groups to colors + index_color = 0 + for group in groups: + # rebuild bmesh data (e.g. left edit mode previous loop) + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + if hasattr(bm.faces, "ensure_lookup_table"): + bm.faces.ensure_lookup_table() + + # Select group + bpy.ops.mesh.select_all(action='DESELECT') + for index_face in group: + bm.faces[index_face].select = True + + # Assign to selection + bpy.ops.uv.textools_color_assign(index=index_color) + + index_color = (index_color+1) % bpy.context.scene.texToolsSettings.color_ID_count + + bpy.ops.object.mode_set(mode='OBJECT') + utilities_color.validate_face_colors(obj) + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_color_from_materials.py b/op_color_from_materials.py new file mode 100644 index 0000000..61d7988 --- /dev/null +++ b/op_color_from_materials.py @@ -0,0 +1,53 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_color + +class op(bpy.types.Operator): + bl_idname = "uv.textools_color_from_materials" + bl_label = "Color Elements" + bl_description = "Assign a color ID to each mesh material slot" + bl_options = {'REGISTER', 'UNDO'} + + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object not in bpy.context.selected_objects: + return False + + if len(bpy.context.selected_objects) != 1: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + def execute(self, context): + color_materials(self, context) + return {'FINISHED'} + + + +def color_materials(self, context): + obj = bpy.context.active_object + + for s in range(len(obj.material_slots)): + slot = obj.material_slots[s] + if slot.material: + utilities_color.assign_slot(obj, s) + + utilities_color.validate_face_colors(obj) + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_color_io_export.py b/op_color_io_export.py new file mode 100644 index 0000000..46cc5f8 --- /dev/null +++ b/op_color_io_export.py @@ -0,0 +1,42 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_color + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_color_io_export" + bl_label = "Export" + bl_description = "Export current color palette to clipboard" + + @classmethod + def poll(cls, context): + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + def execute(self, context): + export_colors(self, context) + return {'FINISHED'} + + + +def export_colors(self, context): + + hex_colors = [] + for i in range(bpy.context.scene.texToolsSettings.color_ID_count): + color = getattr(bpy.context.scene.texToolsSettings, "color_ID_color_{}".format(i)) + hex_colors.append( utilities_color.color_to_hex( color) ) + + bpy.context.window_manager.clipboard = ", ".join(hex_colors) + bpy.ops.ui.textools_popup('INVOKE_DEFAULT', message="{}x colors copied to clipboard".format(len(hex_colors))) + + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_color_io_import.py b/op_color_io_import.py new file mode 100644 index 0000000..5e57a43 --- /dev/null +++ b/op_color_io_import.py @@ -0,0 +1,66 @@ +import bpy +import bmesh +import operator +import string +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_color + +class op(bpy.types.Operator): + bl_idname = "uv.textools_color_io_import" + bl_label = "Import" + bl_description = "Import hex colors from the clipboard as current color palette" + + @classmethod + def poll(cls, context): + + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + def execute(self, context): + import_colors(self, context) + return {'FINISHED'} + + + +def import_colors(self, context): + # Clipboard hex strings + hex_strings = bpy.context.window_manager.clipboard.split(',') + + for i in range(len(hex_strings)): + hex_strings[i] = hex_strings[i].strip().strip('#') + if len(hex_strings[i]) != 6 or not all(c in string.hexdigits for c in hex_strings[i]): + # Incorrect format + self.report({'ERROR_INVALID_INPUT'}, "Incorrect hex format '{}' use a #RRGGBB pattern".format(hex_strings[i])) + return + else: + name = "color_ID_color_{}".format(i) + if hasattr(bpy.context.scene.texToolsSettings, name): + # Color Index exists + color = utilities_color.hex_to_color( hex_strings[i] ) + setattr(bpy.context.scene.texToolsSettings, name, color) + else: + # More colors imported than supported + self.report({'ERROR_INVALID_INPUT'}, "Only {}x colors have been imported instead of {}x".format( + i,len(hex_strings) + )) + return + + # Set number of colors + bpy.context.scene.texToolsSettings.color_ID_count = len(hex_strings) + + bpy.ops.ui.textools_popup('INVOKE_DEFAULT', message="{}x colors imported from clipboard".format( len(hex_strings) )) + +bpy.utils.register_class(op) + + + + + + diff --git a/op_color_select.py b/op_color_select.py new file mode 100644 index 0000000..15a2c23 --- /dev/null +++ b/op_color_select.py @@ -0,0 +1,69 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_color + +class op(bpy.types.Operator): + bl_idname = "uv.textools_color_select" + bl_label = "Assign Color" + bl_description = "Select faces by this color" + bl_options = {'REGISTER', 'UNDO'} + + index : bpy.props.IntProperty(description="Color Index", default=0) + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object not in bpy.context.selected_objects: + return False + + # Allow only 1 object selected + if len(bpy.context.selected_objects) != 1: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + def execute(self, context): + select_color(self, context, self.index) + return {'FINISHED'} + + + +def select_color(self, context, index): + print("Color select "+str(index) ) + + obj = bpy.context.active_object + + # Check for missing slots, materials,.. + if index >= len(obj.material_slots): + self.report({'ERROR_INVALID_INPUT'}, "No material slot for color '{}' found".format(index) ) + return + + if not obj.material_slots[index].material: + self.report({'ERROR_INVALID_INPUT'}, "No material found for material slot '{}'".format(index) ) + return + + if bpy.context.active_object.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + + # Select faces + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + bpy.ops.mesh.select_all(action='DESELECT') + for face in bm.faces: + if face.material_index == index: + face.select = True + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_edge_split_bevel.py b/op_edge_split_bevel.py new file mode 100644 index 0000000..0a36a24 --- /dev/null +++ b/op_edge_split_bevel.py @@ -0,0 +1,375 @@ +import bpy +import os +import bmesh +import math +import operator + +from mathutils import Vector +from collections import defaultdict +from itertools import chain # 'flattens' collection of iterables + +from . import utilities_uv + + + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_edge_split_bevel" + bl_label = "Split Bevel" + bl_description = "..." + bl_options = {'REGISTER', 'UNDO'} + + radius : bpy.props.FloatProperty( + name = "Space", + description = "Space for split bevel", + default = 0.015, + min = 0, + max = 0.35 + ) + + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + + def execute(self, context): + main(self, self.radius) + return {'FINISHED'} + + + +def main(self, radius): + + #Store selection + utilities_uv.selection_store() + + print("____________\nedge split UV sharp edges {}".format(radius)) + + + obj = bpy.context.object + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + islands = utilities_uv.getSelectionIslands() + + + # Collect UV to Vert + vert_to_uv = utilities_uv.get_vert_to_uv(bm, uv_layers) + uv_to_vert = utilities_uv.get_uv_to_vert(bm, uv_layers) + + # Collect hard edges + edges = [] + for edge in bm.edges: + if edge.select and not edge.smooth: + # edge.link_faces + # print("Hard edge: {} - {}".format(edge.verts[0].index, edge.verts[1].index)) + edges.append(edge) + + # Get vert rails to slide + vert_rails = get_vert_edge_rails(edges) + + # Get left and right faces + edge_face_pairs = get_edge_face_pairs(edges) + + + print("Vert rails: {}x".format(len(vert_rails))) + # for vert in vert_rails: + # print(".. v.idx {} = {}x".format(vert.index, len(vert_rails[vert]) )) + + + + vert_processed = [] + vert_uv_pos = [] + + for edge in edges: + if len(edge_face_pairs[edge]) == 2: + v0 = edge.verts[0] + v1 = edge.verts[1] + + f0 = edge_face_pairs[edge][0] + f1 = edge_face_pairs[edge][1] + + # v0 + if v0 not in vert_processed: + vert_processed.append(v0) + faces, origin, delta = slide_uvs(v0, edge, f0, edges, vert_rails, vert_to_uv) + vert_uv_pos.append( {"v":v0, "f":f0, "origin":origin, "delta":delta, "faces":faces} ) + + faces, origin, delta = slide_uvs(v0, edge, f1, edges, vert_rails, vert_to_uv) + vert_uv_pos.append( {"v":v0, "f":f1, "origin":origin, "delta":delta, "faces":faces} ) + + # V1 + if v1 not in vert_processed: + vert_processed.append(v1) + faces, origin, delta = slide_uvs(v1, edge, f0, edges, vert_rails, vert_to_uv) + vert_uv_pos.append( {"v":v1, "f":f0, "origin":origin, "delta":delta, "faces":faces} ) + + faces, origin, delta = slide_uvs(v1, edge, f1, edges, vert_rails, vert_to_uv) + vert_uv_pos.append( {"v":v1, "f":f1, "origin":origin, "delta":delta, "faces":faces} ) + + # ... + for item in vert_uv_pos: + v = item["v"] + + + for face in item["faces"]: + if v in face.verts: + for loop in face.loops: + if loop.vert == v: + loop[uv_layers].uv= item["origin"] + item["delta"] * (radius/2) + # for f in faces: + # for loop in f.loops: + # if loop.vert == vert: + # loop[uv_layers].uv= vert_to_uv[vert][0].uv + item["delta"] * radius/2 + + + + + + # for loop in face.loops: + # if loop.vert == vert: + # loop[uv_layers].uv+= avg_uv_delta + + + + #Restore selection + utilities_uv.selection_restore() + + +def slide_uvs(vert, edge, face, edges, vert_rails, vert_to_uv): + + def IS_DEBUG(): + return vert.index == 64 and edge.verts[0].index == 64 and edge.verts[1].index == 63 + + + A = edge.verts[0] + B = edge.verts[1] + A_links, B_links = get_edge_prev_next(edge, edges) + + verts_edges = {edge.verts[0], edge.verts[1]} + for v in A_links: + verts_edges.add( v ) + for v in B_links: + verts_edges.add( v ) + + if IS_DEBUG(): + print("\r") + + print("Edge {} <--> {} ({})".format(edge.verts[0].index, edge.verts[1].index , vert.index)) + + # Collect faces of this side + + ''' + faces = [face] + face_edges_used = [e for e in face.edges if e in edges] + for e in face.edges: + if e not in face_edges_used: + for f in e.link_faces: + if f != face: + faces.append(f) + ''' + + faces = [face] + edges_main_used = [edge] + for i in range(2): + append = [] + + for f in faces: + for e in f.edges: + if e not in edges_main_used: + if e in edges: + edges_main_used.append(e) + + for f_link in e.link_faces: + if f_link not in faces: + append.append(f_link) + faces.extend(append) + + if IS_DEBUG(): + print(" Faces {}x = {}".format(len(faces), [f.index for f in faces])) + + + # Get all face edges that could be valid rails + face_edges = list(set([e for f in faces for e in f.edges if e not in edges])) + + # The verts influencing the offset + verts = [A,B] + if vert == A: + verts.extend(B_links) + elif vert == B: + verts.extend(A_links) + # verts = [vert] + + if IS_DEBUG(): + print(" Verts: {}x = {}".format(len(verts), [v.index for v in verts])) + print(" Rails:") + + + delta = Vector((0,0)) + count = 0.0 + for v in verts: + rails = [e for e in vert_rails[v] if e in face_edges] + + if IS_DEBUG(): + print(" #{} rails = {}".format(v.index, [("{} - {}".format(e.verts[0].index, e.verts[1].index)) for e in rails])) + + + for e in rails: + # determine order + v0 = None + v1 = None + if e.verts[0] in verts_edges: + v0 = e.verts[0] + v1 = e.verts[1] + elif e.verts[1] in verts_edges: + v0 = e.verts[1] + v1 = e.verts[0] + uv0 = vert_to_uv[v0][0].uv + uv1 = vert_to_uv[v1][0].uv + delta += (uv1-uv0).normalized() + count += 1.0 + + delta/=count + + if IS_DEBUG(): + print("\r") + + return faces, vert_to_uv[vert][0].uv.copy(), delta.normalized() + # print(" V{} = {}".format(v.index, avg_uv_delta)) + + # for loop in face.loops: + # if loop.vert == vert: + # loop[uv_layers].uv+= avg_uv_delta + + + +''' +def slide_face_uvs(uv_layers, edge, vert, face, radius, vert_to_uv): + avg_target = Vector((0,0)) + avg_count = 0 + + for e in face.edges: + if e != edge and vert in e.verts: + vert_B = e.verts[0] + if vert == e.verts[0]: + vert_B = e.verts[1] + A = vert_to_uv[vert][0].uv + B = vert_to_uv[vert_B][0].uv + + avg_target+= A +(B - A).normalized() * radius + avg_count+=1 + + avg_target/=avg_count + avg_target = vert_to_uv[vert][0].uv +(avg_target - vert_to_uv[vert][0].uv).normalized() * radius + + for loop in face.loops: + if loop.vert == vert: + loop[uv_layers].uv = avg_target +''' + + + + + + + + +''' + # Get all rails (max 3x: current, before and after) + rails = [e for v in verts_edges for e in vert_rails[v]] + + for x in rails: + print(" raail: {} x {}".format(x.verts[0].index, x.verts[1].index)) + + # Keep only rails shared with faces + rails = [e for e in rails if e in face_edges] + + + # print("...... v{} with {}x rails ".format(vert.index, len(rails))) + + # Filter rails on same side +''' + + + + +def get_edge_prev_next(edge, edges): + A = edge.verts[0] + B = edge.verts[1] + + # print(" get_edge_prev_next {}x edges".format(len(edges))) + # v0_extends = [] + # v0_extends = [v for e in edges for v in e.verts if v in edge.verts and e != edge and v != v0] + # v1_extends = [v for e in edges for v in e.verts if v in edge.verts and e != edge and v != v1] + # v0_extends = [v_nest for v in edge.verts for e in v.link_edges for v_nest in e.verts if e != edge and if e in edges] + + A_extends = [v2 for v1 in edge.verts for e in v1.link_edges for v2 in e.verts if e != edge and e in edges and v2 not in edge.verts and v1 != A] + B_extends = [v2 for v1 in edge.verts for e in v1.link_edges for v2 in e.verts if e != edge and e in edges and v2 not in edge.verts and v1 != B] + + return A_extends, B_extends + + +def get_edge_face_pairs(edges): + edge_faces = {} + for edge in edges: + v0 = edge.verts[0] + v1 = edge.verts[1] + faces = [] + for face in edge.link_faces: + if v0 in face.verts and v1 in face.verts: + faces.append(face) + edge_faces[edge] = faces + + return edge_faces + + + +def get_vert_edge_rails(edges): + + vert_rails = {} + for edge in edges: + v0 = edge.verts[0] + v1 = edge.verts[1] + + faces = [] + for face in edge.link_faces: + if v0 in face.verts and v1 in face.verts: + faces.append(face) + + for face in faces: + for e in face.edges: + if e not in edges and len(e.link_faces) > 0: + if v0 not in vert_rails: + vert_rails[ v0 ] = [] + if v1 not in vert_rails: + vert_rails[ v1 ] = [] + + if v0 in e.verts and e not in vert_rails[v0]: + vert_rails[v0].append(e) + + if v1 in e.verts and e not in vert_rails[v1]: + vert_rails[v1].append(e) + + return vert_rails + + +bpy.utils.register_class(op) diff --git a/op_island_align_edge.py b/op_island_align_edge.py new file mode 100644 index 0000000..13eeadc --- /dev/null +++ b/op_island_align_edge.py @@ -0,0 +1,142 @@ +import bpy +import bmesh +import operator +import math +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv + +class op(bpy.types.Operator): + bl_idname = "uv.textools_island_align_edge" + bl_label = "Align Island by Edge" + bl_description = "Align the island by selected edge" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + # Requires UV Edge select mode + if bpy.context.scene.tool_settings.uv_select_mode != 'EDGE': + return False + + return True + + + def execute(self, context): + #Store selection + utilities_uv.selection_store() + + main(context) + + #Restore selection + utilities_uv.selection_restore() + + return {'FINISHED'} + + + +def main(context): + print("Executing operator_island_align_edge") + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + faces_selected = []; + for face in bm.faces: + if face.select: + for loop in face.loops: + if loop[uv_layers].select: + faces_selected.append(face) + break + + print("faces_selected: "+str(len(faces_selected))) + + # Collect 2 uv verts for each island + face_uvs = {} + for face in faces_selected: + uvs = [] + for loop in face.loops: + if loop[uv_layers].select: + uvs.append(loop[uv_layers]) + if len(uvs) >= 2: + break + if len(uvs) >= 2: + face_uvs[face] = uvs + + faces_islands = {} + faces_unparsed = faces_selected.copy() + for face in face_uvs: + if face in faces_unparsed: + + bpy.ops.uv.select_all(action='DESELECT') + face_uvs[face][0].select = True; + bpy.ops.uv.select_linked()#Extend selection + + #Collect faces + faces_island = [face]; + for f in faces_unparsed: + if f != face and f.select and f.loops[0][uv_layers].select: + print("append "+str(f.index)) + faces_island.append(f) + for f in faces_island: + faces_unparsed.remove(f) + + #Assign Faces to island + faces_islands[face] = faces_island + + print("Sets: {}x".format(len(faces_islands))) + + # Align each island to its edges + for face in faces_islands: + align_island(face_uvs[face][0].uv, face_uvs[face][1].uv, faces_islands[face]) + + +def align_island(uv_vert0, uv_vert1, faces): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + print("Align {}x faces".format(len(faces))) + + # Select faces + bpy.ops.uv.select_all(action='DESELECT') + for face in faces: + for loop in face.loops: + loop[uv_layers].select = True + + diff = uv_vert1 - uv_vert0 + angle = math.atan2(diff.y, diff.x)%(math.pi/2) + + bpy.ops.uv.select_linked() + + bpy.context.tool_settings.transform_pivot_point = 'CURSOR' + bpy.ops.uv.cursor_set(location=uv_vert0 + diff/2) + + if angle >= (math.pi/4): + angle = angle - (math.pi/2) + + bpy.ops.transform.rotate(value=angle, orient_axis='Z', constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False) + + +bpy.utils.register_class(op) diff --git a/op_island_align_sort.py b/op_island_align_sort.py new file mode 100644 index 0000000..6b9df02 --- /dev/null +++ b/op_island_align_sort.py @@ -0,0 +1,171 @@ +import bpy +import bmesh +import operator +import math + +from mathutils import Vector +from collections import defaultdict + + +from . import utilities_uv +import imp +imp.reload(utilities_uv) + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_island_align_sort" + bl_label = "Align & Sort" + bl_description = "Rotates UV islands to minimal bounds and sorts them horizontal or vertical" + bl_options = {'REGISTER', 'UNDO'} + + + is_vertical : bpy.props.BoolProperty(description="Vertical or Horizontal orientation", default=True) + padding : bpy.props.FloatProperty(description="Padding between UV islands", default=0.05) + + @classmethod + def poll(cls, context): + + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False #self.report({'WARNING'}, "Object must have more than one UV map") + + #Not in Synced mode + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + + return True + + + def execute(self, context): + main(context, self.is_vertical, self.padding) + return {'FINISHED'} + + +def main(context, isVertical, padding): + print("Executing IslandsAlignSort main {}".format(padding)) + + #Store selection + utilities_uv.selection_store() + + if bpy.context.tool_settings.transform_pivot_point != 'CURSOR': + bpy.context.tool_settings.transform_pivot_point = 'CURSOR' + + #Only in Face or Island mode + if bpy.context.scene.tool_settings.uv_select_mode is not 'FACE' or 'ISLAND': + bpy.context.scene.tool_settings.uv_select_mode = 'FACE' + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + + boundsAll = utilities_uv.getSelectionBBox() + + + islands = utilities_uv.getSelectionIslands() + allSizes = {} #https://stackoverflow.com/questions/613183/sort-a-python-dictionary-by-value + allBounds = {} + + print("Islands: "+str(len(islands))+"x") + + bpy.context.window_manager.progress_begin(0, len(islands)) + + #Rotate to minimal bounds + for i in range(0, len(islands)): + alignIslandMinimalBounds(uv_layers, islands[i]) + + # Collect BBox sizes + bounds = utilities_uv.getSelectionBBox() + allSizes[i] = max(bounds['width'], bounds['height']) + i*0.000001;#Make each size unique + allBounds[i] = bounds; + print("Rotate compact: "+str(allSizes[i])) + + bpy.context.window_manager.progress_update(i) + + bpy.context.window_manager.progress_end() + + + #Position by sorted size in row + sortedSizes = sorted(allSizes.items(), key=operator.itemgetter(1))#Sort by values, store tuples + sortedSizes.reverse() + offset = 0.0 + for sortedSize in sortedSizes: + index = sortedSize[0] + island = islands[index] + bounds = allBounds[index] + + #Select Island + bpy.ops.uv.select_all(action='DESELECT') + utilities_uv.set_selected_faces(island) + + #Offset Island + if(isVertical): + delta = Vector((boundsAll['min'].x - bounds['min'].x, boundsAll['max'].y - bounds['max'].y)); + bpy.ops.transform.translate(value=(delta.x, delta.y-offset, 0)) + offset += bounds['height']+padding + else: + print("Horizontal") + delta = Vector((boundsAll['min'].x - bounds['min'].x, boundsAll['max'].y - bounds['max'].y)); + bpy.ops.transform.translate(value=(delta.x+offset, delta.y, 0)) + offset += bounds['width']+padding + + + #Restore selection + utilities_uv.selection_restore() + + +def alignIslandMinimalBounds(uv_layers, faces): + # Select Island + bpy.ops.uv.select_all(action='DESELECT') + utilities_uv.set_selected_faces(faces) + + steps = 8 + angle = 45; # Starting Angle, half each step + + bboxPrevious = utilities_uv.getSelectionBBox() + + for i in range(0, steps): + # Rotate right + bpy.ops.transform.rotate(value=(angle * math.pi / 180), orient_axis='Z') + bbox = utilities_uv.getSelectionBBox() + + if i == 0: + sizeA = bboxPrevious['width'] * bboxPrevious['height'] + sizeB = bbox['width'] * bbox['height'] + if abs(bbox['width'] - bbox['height']) <= 0.0001 and sizeA < sizeB: + # print("Already squared") + bpy.ops.transform.rotate(value=(-angle * math.pi / 180), orient_axis='Z') + break; + + + if bbox['minLength'] < bboxPrevious['minLength']: + bboxPrevious = bbox; # Success + else: + # Rotate Left + bpy.ops.transform.rotate(value=(-angle*2 * math.pi / 180), orient_axis='Z') + bbox = utilities_uv.getSelectionBBox() + if bbox['minLength'] < bboxPrevious['minLength']: + bboxPrevious = bbox; # Success + else: + # Restore angle of this iteration + bpy.ops.transform.rotate(value=(angle * math.pi / 180), orient_axis='Z') + + angle = angle / 2 + + if bboxPrevious['width'] < bboxPrevious['height']: + bpy.ops.transform.rotate(value=(90 * math.pi / 180), orient_axis='Z') + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_island_align_world.py b/op_island_align_world.py new file mode 100644 index 0000000..a552d4d --- /dev/null +++ b/op_island_align_world.py @@ -0,0 +1,291 @@ +import bpy +import os +import bmesh +import math +import operator + +from mathutils import Vector +from collections import defaultdict +from itertools import chain # 'flattens' collection of iterables + +from . import utilities_uv + + + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_island_align_world" + bl_label = "Align World" + bl_description = "Align selected UV islands to world / gravity directions" + bl_options = {'REGISTER', 'UNDO'} + + # is_global = bpy.props.BoolProperty( + # name = "Global Axis", + # description = "Global or local object axis alignment", + # default = False + # ) + + # def draw(self, context): + # layout = self.layout + # layout.prop(self, "is_global") + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + return True + + + def execute(self, context): + main(self) + return {'FINISHED'} + + + +def main(context): + print("\n________________________\nis_global") + + #Store selection + utilities_uv.selection_store() + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + #Only in Face or Island mode + if bpy.context.scene.tool_settings.uv_select_mode is not 'FACE' or 'ISLAND': + bpy.context.scene.tool_settings.uv_select_mode = 'FACE' + + obj = bpy.context.object + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + islands = utilities_uv.getSelectionIslands() + + + + for faces in islands: + # Get average viewport normal of UV island + avg_normal = Vector((0,0,0)) + for face in faces: + avg_normal+=face.normal + avg_normal/=len(faces) + + # avg_normal = (obj.matrix_world*avg_normal).normalized() + + # Which Side + x = 0 + y = 1 + z = 2 + max_size = max(abs(avg_normal.x), abs(avg_normal.y), abs(avg_normal.z)) + + # Use multiple steps + for i in range(3): + if(abs(avg_normal.x) == max_size): + print("x normal") + align_island(obj, bm, uv_layers, faces, y, z, avg_normal.x < 0, False) + + elif(abs(avg_normal.y) == max_size): + print("y normal") + align_island(obj, bm, uv_layers, faces, x, z, avg_normal.y > 0, False) + + elif(abs(avg_normal.z) == max_size): + print("z normal") + align_island(obj, bm, uv_layers, faces, x, y, False, avg_normal.z < 0) + + print("align island: faces {}x n:{}, max:{}".format(len(faces), avg_normal, max_size)) + + + + #Restore selection + utilities_uv.selection_restore() + + + +def align_island(obj, bm, uv_layers, faces, x=0, y=1, flip_x=False, flip_y=False): + + # Find lowest and highest verts + minmax_val = [0,0] + minmax_vert = [None, None] + + axis_names = ['x', 'y', 'z'] + print("Align shell {}x at {},{} flip {},{}".format(len(faces), axis_names[x], axis_names[y], flip_x, flip_y)) + + + # print(" Min #{} , Max #{} along '{}'".format(minmax_vert[0].index, minmax_vert[1].index, axis_names[y] )) + # print(" A1 {:.1f} , A2 {:.1f} along ".format(minmax_val[0], minmax_val[1] )) + + + + # Collect UV to Vert + vert_to_uv = utilities_uv.get_vert_to_uv(bm, uv_layers) + uv_to_vert = utilities_uv.get_uv_to_vert(bm, uv_layers) + + processed_edges = [] + edges = [] + for face in faces: + for edge in face.edges: + if edge not in processed_edges: + processed_edges.append(edge) + delta = edge.verts[0].co -edge.verts[1].co + max_side = max(abs(delta.x), abs(delta.y), abs(delta.z)) + + # Check edges dominant in active axis + if( abs(delta[x]) == max_side or abs(delta[y]) == max_side): + # if( abs(delta[y]) == max_side): + edges.append(edge) + + print("Edges {}x".format(len(edges))) + + avg_angle = 0 + for edge in edges: + uv0 = vert_to_uv[ edge.verts[0] ][0] + uv1 = vert_to_uv[ edge.verts[1] ][0] + delta_verts = Vector(( + edge.verts[1].co[x] - edge.verts[0].co[x], + edge.verts[1].co[y] - edge.verts[0].co[y] + )) + + + if flip_x: + delta_verts.x = -edge.verts[1].co[x] + edge.verts[0].co[x] + if flip_y: + delta_verts.y = -edge.verts[1].co[y] + edge.verts[0].co[y] + + # delta_verts.y = edge.verts[0].co[y] - edge.verts[1].co[y] + + + delta_uvs = Vector(( + uv1.uv.x - uv0.uv.x, + uv1.uv.y - uv0.uv.y + )) + a0 = math.atan2(delta_verts.y, delta_verts.x) - math.pi/2 + a1 = math.atan2(delta_uvs.y, delta_uvs.x) - math.pi/2 + + + + + a_delta = math.atan2(math.sin(a0-a1), math.cos(a0-a1)) + # edge.verts[0].index, edge.verts[1].index + # print(" turn {:.1f} .. {:.1f} , {:.1f}".format(a_delta*180/math.pi, a0*180/math.pi,a1*180/math.pi)) + avg_angle+=a_delta + + + avg_angle/=len(edges) # - math.pi/2 + print("Turn {:.1f}".format(avg_angle * 180/math.pi)) + + bpy.ops.uv.select_all(action='DESELECT') + for face in faces: + for loop in face.loops: + loop[uv_layers].select = True + + + bpy.context.tool_settings.transform_pivot_point = 'MEDIAN_POINT' + bpy.ops.transform.rotate(value=avg_angle, orient_axis='Z') + # bpy.ops.transform.rotate(value=0.58191, axis=(-0, -0, -1), constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SPHERE', proportional_size=0.0267348) + + + # processed = [] + + + ''' + bpy.ops.uv.select_all(action='DESELECT') + for face in faces: + + # Collect UV to Vert + for loop in face.loops: + loop[uv_layers].select = True + vert = loop.vert + uv = loop[uv_layers] + # vert_to_uv + if vert not in vert_to_uv: + vert_to_uv[vert] = [uv]; + else: + vert_to_uv[vert].append(uv) + # uv_to_vert + if uv not in uv_to_vert: + uv_to_vert[ uv ] = vert; + + + for vert in face.verts: + if vert not in processed: + processed.append(vert) + + vert_y = (vert.co)[y] #obj.matrix_world * + + print("idx {} = {}".format(vert.index, vert_y)) + + if not minmax_vert[0] or not minmax_vert[1]: + minmax_vert[0] = vert + minmax_vert[1] = vert + minmax_val[0] = vert_y + minmax_val[1] = vert_y + continue + + if vert_y < minmax_val[0]: + # Not yet defined or smaller + minmax_vert[0] = vert + minmax_val[0] = vert_y + + elif vert_y > minmax_val[1]: + minmax_vert[1] = vert + minmax_val[1] = vert_y + + + if minmax_vert[0] and minmax_vert[1]: + axis_names = ['x', 'y', 'z'] + print(" Min #{} , Max #{} along '{}'".format(minmax_vert[0].index, minmax_vert[1].index, axis_names[y] )) + # print(" A1 {:.1f} , A2 {:.1f} along ".format(minmax_val[0], minmax_val[1] )) + + vert_A = minmax_vert[0] + vert_B = minmax_vert[1] + uv_A = vert_to_uv[vert_A][0] + uv_B = vert_to_uv[vert_B][0] + + delta_verts = Vector(( + vert_B.co[x] - vert_A.co[x], + vert_B.co[y] - vert_A.co[y] + )) + + delta_uvs = Vector(( + uv_B.uv.x - uv_A.uv.x, + uv_B.uv.y - uv_A.uv.y, + + )) + # Get angles + angle_vert = math.atan2(delta_verts.y, delta_verts.x) - math.pi/2 + angle_uv = math.atan2(delta_uvs.y, delta_uvs.x) - math.pi/2 + + angle_delta = math.atan2(math.sin(angle_vert-angle_uv), math.cos(angle_vert-angle_uv)) + + print(" Angles {:.2f} | {:.2f}".format(angle_vert*180/math.pi, angle_uv*180/math.pi)) + print(" Angle Diff {:.2f}".format(angle_delta*180/math.pi)) + + bpy.context.tool_settings.transform_pivot_point = 'MEDIAN_POINT' + bpy.ops.transform.rotate(value=angle_delta, axis='Z') + # bpy.ops.transform.rotate(value=0.58191, axis=(-0, -0, -1), constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SPHERE', proportional_size=0.0267348) + + + # bpy.ops.mesh.select_all(action='DESELECT') + # vert_A.select = True + # vert_B.select = True + + # return + ''' + +bpy.utils.register_class(op) diff --git a/op_island_mirror.py b/op_island_mirror.py new file mode 100644 index 0000000..5538afd --- /dev/null +++ b/op_island_mirror.py @@ -0,0 +1,783 @@ +import bpy +import os +import bmesh +import math +import operator +from mathutils import Vector +from collections import defaultdict +from itertools import chain # 'flattens' collection of iterables + +from . import utilities_uv + + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_island_mirror" + bl_label = "Symmetry" + bl_description = "Mirrors selected faces to other half or averages based on selected edge center" + bl_options = {'REGISTER', 'UNDO'} + + is_stack : bpy.props.BoolProperty(description="Stack the halves on top of each other?", default=False) + + @classmethod + def poll(cls, context): + + + if not bpy.context.active_object: + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + + + if bpy.context.scene.tool_settings.uv_select_mode != 'EDGE' and bpy.context.scene.tool_settings.uv_select_mode != 'FACE': + return False + + # if bpy.context.scene.tool_settings.use_uv_select_sync: + # return False + + return True + + def execute(self, context): + main(context) + return {'FINISHED'} + + + +def main(context): + print("--------------------------- Executing operator_mirror") + + #Store selection + utilities_uv.selection_store() + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + if bpy.context.scene.tool_settings.uv_select_mode == 'EDGE': + + + # 1.) Collect left and right side verts + verts_middle = []; + + for face in bm.faces: + if face.select: + for loop in face.loops: + if loop[uv_layers].select and loop.vert not in verts_middle: + verts_middle.append(loop.vert) + + # 2.) Align UV shell + alignToCenterLine() + + # Convert to Vert selection and extend edge loop in 3D space + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') + bpy.ops.mesh.select_all(action='DESELECT') + for vert in verts_middle: + vert.select = True + + bpy.ops.mesh.select_mode(use_extend=True, use_expand=False, type='EDGE') + bpy.ops.mesh.loop_multi_select(ring=False) + for vert in bm.verts: + if vert.select and vert not in verts_middle: + print("Append extra vert to symmetry line from xyz edge loop") + verts_middle.append(vert) + + # Select UV shell Again + bpy.ops.mesh.select_linked(delimit={'UV'}) + verts_island = [] + for vert in bm.verts: + if vert.select: + verts_island.append(vert) + + + # 3.) Restore UV vert selection + x_middle = 0 + bpy.ops.uv.select_all(action='DESELECT') + for face in bm.faces: + if face.select: + for loop in face.loops: + if loop.vert in verts_middle: + loop[uv_layers].select = True + x_middle = loop[uv_layers].uv.x; + + + print("Middle "+str(len(verts_middle))+"x, x pos: "+str(x_middle)) + + # Extend selection + bpy.ops.uv.select_more() + verts_A = []; + verts_B = []; + for face in bm.faces: + if face.select: + for loop in face.loops: + if loop[uv_layers].select and loop.vert not in verts_middle: + if loop[uv_layers].uv.x <= x_middle: + # Left + if loop.vert not in verts_A: + verts_A.append(loop.vert) + + elif loop[uv_layers].uv.x > x_middle: + # Right + if loop.vert not in verts_B: + verts_B.append(loop.vert) + + + + + def remove_doubles(): + verts_double = [vert for vert in verts_island if (vert in verts_A and vert in verts_B)] + + # print("Temp double: "+str(len(verts_double))+"x") + if len(verts_double) > 0: + print("TODO: Remove doubles "+str(len(verts_double))) + for vert in verts_double: + verts_A.remove(vert) + verts_B.remove(vert) + verts_middle.append(vert) + + def extend_half_selection(verts_middle, verts_half, verts_other): + # Select initial half selection + bpy.ops.uv.select_all(action='DESELECT') + for face in bm.faces: + if face.select: + for loop in face.loops: + if loop.vert in verts_half: + loop[uv_layers].select = True + + # Extend selection + bpy.ops.uv.select_more() + + # count_added = 0 + for face in bm.faces: + if face.select: + for loop in face.loops: + if loop.vert not in verts_half and loop.vert not in verts_middle and loop[uv_layers].select: + verts_half.append(loop.vert) + + + remove_doubles() + + # Limit iteration loops + max_loops_extend = 200 + for i in range(0, max_loops_extend): + print("Now extend selection A / B") + count_hash = str(len(verts_A))+"_"+str(len(verts_B)); + extend_half_selection(verts_middle, verts_A, verts_B) + extend_half_selection(verts_middle, verts_B, verts_A) + remove_doubles() + + count_hash_new = str(len(verts_A))+"_"+str(len(verts_B)); + if count_hash_new == count_hash: + print("Break loop, same as previous loop") + break; + + print("Edge, Sides: L:"+str(len(verts_A))+" | R:"+str(len(verts_B))) + + # 4.) Mirror Verts + mirror_verts(verts_middle, verts_A, verts_B, False) + + + if bpy.context.scene.tool_settings.uv_select_mode == 'FACE': + + # 1.) Get selected UV faces to vert faces + selected_faces = [] + for face in bm.faces: + if face.select: + # Are all UV faces selected? + countSelected = 0 + for loop in face.loops: + if loop[uv_layers].select: + countSelected+=1 + # print("Vert selected "+str(face.index)) + if countSelected == len(face.loops): + selected_faces.append(face) + + + # if bpy.context.scene.tool_settings.use_uv_select_sync == False: + + bpy.ops.uv.select_linked() + verts_all = [] + for face in bm.faces: + if face.select: + for loop in face.loops: + if(loop.vert not in verts_all): + verts_all.append(loop.vert) + + print("Verts shell: "+str(len(verts_all))) + + + bpy.ops.mesh.select_all(action='DESELECT') + for face in selected_faces: + face.select = True + + + # 2.) Select Vert shell's outer edges + bpy.ops.mesh.select_linked(delimit=set()) + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + bpy.ops.mesh.region_to_loop() + edges_outer_shell = [e for e in bm.edges if e.select] + + # 3.) Select Half's outer edges + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + bpy.ops.mesh.select_all(action='DESELECT') + for face in selected_faces: + face.select = True + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + bpy.ops.mesh.region_to_loop() + edges_outer_selected = [e for e in bm.edges if e.select] + + # 4.) Mask edges exclusive to edges_outer_selected (symmetry line) + edges_middle = [item for item in edges_outer_selected if item not in edges_outer_shell] + + # 5.) Convert to UV selection + verts_middle = [] + for edge in edges_middle: + if edge.verts[0] not in verts_middle: + verts_middle.append(edge.verts[0]) + if edge.verts[1] not in verts_middle: + verts_middle.append(edge.verts[1]) + + #Select all Vert shell faces + bpy.ops.mesh.select_linked(delimit=set()) + #Select UV matching vert array + bpy.ops.uv.select_all(action='DESELECT') + for face in bm.faces: + if face.select: + for loop in face.loops: + if loop.vert in verts_middle: + loop[uv_layers].select = True + + # 5.) Align UV shell + alignToCenterLine() + + # 7.) Collect left and right side verts + verts_A = []; + verts_B = []; + + bpy.ops.uv.select_all(action='DESELECT') + for face in selected_faces: + for loop in face.loops: + if loop.vert not in verts_middle and loop.vert not in verts_A: + verts_A.append(loop.vert) + + for vert in verts_all: + if vert not in verts_middle and vert not in verts_A and vert not in verts_B: + verts_B.append(vert) + + # 8.) Mirror Verts + mirror_verts(verts_middle, verts_A, verts_B, True) + + #Restore selection + # utilities_uv.selection_restore() + + + + + +def mirror_verts(verts_middle, verts_A, verts_B, isAToB): + + print("--------------------------------\nMirror: C:"+str(len(verts_middle))+" ; verts: "+str(len(verts_A))+"|"+str(len(verts_B))+"x, A to B? "+str(isAToB)) + + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + + # Get verts_island + verts_island = [] + for vert in verts_middle: + verts_island.append(vert) + for vert in verts_A: + verts_island.append(vert) + for vert in verts_B: + verts_island.append(vert) + + # Select Island as Faces + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') + bpy.ops.mesh.select_all(action='DESELECT') + for vert in verts_island: + vert.select = True + bpy.ops.mesh.select_mode(use_extend=False, use_expand=True, type='FACE') + + # Collect Librarys of verts / UV + vert_to_uv = utilities_uv.get_vert_to_uv(bm, uv_layers) + uv_to_vert = utilities_uv.get_uv_to_vert(bm, uv_layers) + uv_to_face = {} + # UV clusters / groups (within 0.000001 distance) + clusters = [] + uv_to_clusters = {} + vert_to_clusters = {} + + for face in bm.faces: + if face.select: + for loop in face.loops: + vert = loop.vert + uv = loop[uv_layers] + + if uv not in uv_to_face: + uv_to_face[ uv ] = face; + + # clusters + isMerged = False + for cluster in clusters: + d = (uv.uv - cluster.uvs[0].uv).length + if d <= 0.0000001: + #Merge + cluster.append(uv) + uv_to_clusters[uv] = cluster + if vert not in vert_to_clusters: + vert_to_clusters[vert] = cluster + isMerged = True; + break; + if not isMerged: + #New Group + clusters.append( UVCluster(vert, [uv]) ) + uv_to_clusters[uv] = clusters[-1] + if vert not in vert_to_clusters: + vert_to_clusters[vert] = clusters[-1] + + # Get Center X + x_middle = vert_to_uv[ verts_middle[0] ][0].uv.x; + + + # 3.) Grow layer by layer + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') + bpy.context.scene.tool_settings.uv_select_mode = 'VERTEX' + + + clusters_processed = [] + + def select_extend_filter(clusters_border, clusters_mask): + # print("Extend A/B") + connected_clusters = [] + for cluster in clusters_border: + + # Select and Extend selection + bpy.ops.uv.select_all(action='DESELECT') + for uv in cluster.uvs: + uv.select = True + bpy.ops.uv.select_more() + + # Collect extended + uv_extended = [uv for clusterMask in clusters_mask for uv in clusterMask.uvs if (uv.select and clusterMask not in clusters_processed)] + clusters_extended = [] + for uv in uv_extended: + if uv_to_clusters[uv] not in clusters_extended: + clusters_extended.append( uv_to_clusters[uv] ) + + # Sort by distance + groups_distance = {} + for i in range(0, len(clusters_extended)): + sub_group = clusters_extended[i] + groups_distance[i] = (cluster.uvs[0].uv - sub_group.uvs[0].uv).length + + # Append to connected clusters + array = [] + for item in sorted(groups_distance.items(), key=operator.itemgetter(1)): + key = item[0] + clust = clusters_extended[key] + array.append( clust ) + if clust not in clusters_processed: + clusters_processed.append(clust) + + connected_clusters.append( array ) + + if cluster not in clusters_processed: + clusters_processed.append( cluster ) + + + bpy.ops.uv.select_all(action='DESELECT') + for uv in uv_extended: + uv.select = True + + + return connected_clusters + + + + + + + mask_A = [vert_to_clusters[vert] for vert in verts_A] + mask_B = [vert_to_clusters[vert] for vert in verts_B] + + border_A = list([vert_to_clusters[vert] for vert in verts_middle]) + border_B = list([vert_to_clusters[vert] for vert in verts_middle]) + + for step in range(0, 8): + + if len(border_A) == 0: + print("{}.: Finished scanning with no growth iterations".format(step)) + break; + if len(border_A) != len(border_B) or len(border_A) == 0: + print("Abort: non compatible border A/B: {}x {}x ".format(len(border_A), len(border_B))) + break; + + print("{}.: border {}x|{}x, processed: {}x".format(step, len(border_A), len(border_B), len(clusters_processed))) + + # Collect connected pairs for each side + connected_A = select_extend_filter(border_A, mask_A) + connected_B = select_extend_filter(border_B, mask_B) + + print(" Connected: {}x|{}x".format(len(connected_A), len(connected_B))) + + border_A.clear() + border_B.clear() + + # Traverse through pairs + for i in range(0, min(len(connected_A), len(connected_B)) ): + if len(connected_A[i]) == 0: + continue + if len(connected_A[i]) != len(connected_B[i]): + print(". Error: Inconsistent grow mappings from {} {}x | {}x".format(i, len(connected_A[i]), len(connected_B[i]) )) + continue + + indexA = [cluster.vertex.index for cluster in connected_A[i] ] + indexB = [cluster.vertex.index for cluster in connected_B[i] ] + indexA = str(indexA).replace("[","").replace("]","").replace(" ","") + indexB = str(indexB).replace("[","").replace("]","").replace(" ","") + print(". Map {}|{} = {}x|{}x".format(indexA, indexB, len(connected_A[i]), len(connected_B[i]) ) ) + + + + if True:#isAToB: + # Copy A side to B + for cluster in connected_B[i]: + for uv in cluster.uvs: + pos = connected_A[i][0].uvs[0].uv.copy() + pos.x = x_middle - (pos.x-x_middle)# Flip cooreindate + # uv.uv = pos + + # border_A[i] = uv_to_clusters[ connected_A[i][0] ] + # border_B[i] = uv_to_clusters[ connected_B[i][0] ] + for j in range(len(connected_A[i])): + border_A.append( connected_A[i][j] ) + border_B.append( connected_B[i][j] ) + + + + # for uv in clusters_B[idxB]: + # pos = clusters_A[idxA][0].uv.copy() + # # Flip cooreindate + # pos.x = x_middle - (pos.x-x_middle) + # uv.uv = pos + + # for j in range(0, len(connected_A[i])): + # # Group A and B + # groupA = connected_A[i][j]; + # groupB = connected_B[i][j]; + # # vertexA = [vert_to_clusters[key] for key in vert_to_clusters if vert_to_clusters[key] == groupA] + # print("...map {} -> {}".format(groupA, groupB)) + + + + # for j in range(0, count): + # if len(connected_A[j]) != len(connected_B[j]): + # # print("Error: Inconsistent grow mappings from {}:{}x | {}:{}x".format(border_A[j].index,len(connected_A[j]), border_B[j].index, len(connected_B[j]) )) + # print("Error: Inconsistent grow mappings from {} {}x | {}x".format(j, len(connected_A[j]), len(connected_B[j]) )) + # continue + + # for k in range(0, len(connected_A[j])): + # # Vertex A and B + # vA = connected_A[j][k]; + # vB = connected_B[j][k]; + + # uvsA = vert_to_uv[vA]; + # uvsB = vert_to_uv[vB]; + + # clusters_A = collect_clusters(uvsA) + # clusters_B = collect_clusters(uvsB) + + # if len(clusters_A) != len(clusters_B): + # print("Error: Inconsistent vertex UV group pairs at vertex {} : {}".format(vA.index, vB.index)) + # continue + + + # message= "...Map {0} -> {1} = UVs {2}|{3}x | UV-Groups {4}x|{5}x".format( vA.index, vB.index, len(uvsA), len(uvsB), len(clusters_A), len(clusters_B) ) + # if len(clusters_A) > 1: + # message = ">> "+message + # print(message) + + + + # if len(clusters_A) > 0: + # # For each group + + # sortA = {} + # sortB = {} + # for g in range(0, len(clusters_A)): + # uv_A = clusters_A[g][0].uv.copy() + # uv_B = clusters_B[g][0].uv.copy() + + # # localize X values (from symmetry line) + # uv_A.x = (uv_A.x - x_middle) + # uv_B.x = (uv_B.x - x_middle) + + # sortA[g] = abs(uv_A.x) + uv_A.y*2.0 + # sortB[g] = abs(uv_B.x) + uv_B.y*2.0 + # # print(" . [{}] : {:.2f}, {:.2f} | {:.2f}, {:.2f}".format(g, uv_A.x, uv_A.y, uv_B.x, uv_B.y)) + # print(" . [{}] : {:.2f} | {:.2f}".format(g, sortA[g], sortB[g])) + + # # Sort sortA by value + # sortedA = sorted(sortA.items(), key=operator.itemgetter(1)) + # sortedB = sorted(sortB.items(), key=operator.itemgetter(1)) + + # for g in range(0, len(clusters_A)): + # # sortedA[g] + # idxA = sortedA[g][0] + # idxB = sortedB[g][0] + + # print("Map clusters_A {} -> ".format(idxA, idxB)) + # for uv in clusters_B[idxB]: + # pos = clusters_A[idxA][0].uv.copy() + # # Flip cooreindate + # pos.x = x_middle - (pos.x-x_middle) + # uv.uv = pos + + # border_A.append(vA) + # border_B.append(vB) + + + + print("--------------------------------") + ''' + + def select_extend_filter(verts_border, verts_mask): + # print("Extend A/B") + connected_verts = [] + for i in range(0, len(verts_border)): + # Collect connected edge verts + verts_connected_edges = [] + for edge in verts_border[i].link_edges: + if(edge.verts[0] not in verts_connected_edges): + verts_connected_edges.append(edge.verts[0]) + if(edge.verts[1] not in verts_connected_edges): + verts_connected_edges.append(edge.verts[1]) + + # Select vert on border + bpy.ops.mesh.select_all(action='DESELECT') + verts_border[i].select = True + + + # Extend selection + bpy.ops.mesh.select_more() + + # Filter selected verts against mask, connected edges, processed and border + verts_extended = [vert for vert in bm.verts if (vert.select and vert in verts_connected_edges and vert in verts_mask and vert and vert not in verts_border and vert not in verts_processed)] + + + # print(" "+str(i)+". scan: "+str(verts_border[i].index)+"; ext: "+str(len(verts_extended))+"x") + + connected_verts.append( [] ) + + # Sort by distance + verts_distance = {} + for vert in verts_extended: + verts_distance[vert] = (verts_border[i].co - vert.co).length + + for item in sorted(verts_distance.items(), key=operator.itemgetter(1)): + connected_verts[i].append( item[0] ) + + if verts_border[i] not in verts_processed: + verts_processed.append(verts_border[i]) + + return connected_verts + + # find UV vert blobs , see which ones are same spot + def collect_clusters(uvs): + groups = [] + for uv in uvs: + if len(groups) == 0: + groups.append([uv]) + else: + isMerged = False + for group in groups: + d = (uv.uv - group[0].uv).length + if d <= 0.0000001: + #Merge + group.append(uv) + isMerged = True; + break; + if not isMerged: + #New Group + groups.append([uv]) + return groups + + + border_A = [vert for vert in verts_middle] + border_B = [vert for vert in verts_middle] + + + for i in range(0, 200): + + if len(border_A) == 0: + print("Finished scanning at {} growth iterations".format(i)) + break; + if len(border_A) != len(border_B) or len(border_A) == 0: + print("Abort: non compatible border A/B: {}x {}x ".format(len(border_A), len(border_B))) + break; + + connected_A = select_extend_filter(border_A, verts_A) + connected_B = select_extend_filter(border_B, verts_B) + + print("Map pairs: {}|{}".format(len(connected_A), len(connected_B))) + + border_A.clear() + border_B.clear() + + count = min(len(connected_A), len(connected_B)) + for j in range(0, count): + if len(connected_A[j]) != len(connected_B[j]): + # print("Error: Inconsistent grow mappings from {}:{}x | {}:{}x".format(border_A[j].index,len(connected_A[j]), border_B[j].index, len(connected_B[j]) )) + print("Error: Inconsistent grow mappings from {} {}x | {}x".format(j, len(connected_A[j]), len(connected_B[j]) )) + continue + + for k in range(0, len(connected_A[j])): + # Vertex A and B + vA = connected_A[j][k]; + vB = connected_B[j][k]; + + uvsA = vert_to_uv[vA]; + uvsB = vert_to_uv[vB]; + + clusters_A = collect_clusters(uvsA) + clusters_B = collect_clusters(uvsB) + + if len(clusters_A) != len(clusters_B): + print("Error: Inconsistent vertex UV group pairs at vertex {} : {}".format(vA.index, vB.index)) + continue + + + message= "...Map {0} -> {1} = UVs {2}|{3}x | UV-Groups {4}x|{5}x".format( vA.index, vB.index, len(uvsA), len(uvsB), len(clusters_A), len(clusters_B) ) + if len(clusters_A) > 1: + message = ">> "+message + print(message) + + + + if len(clusters_A) > 0: + # For each group + + + + sortA = {} + sortB = {} + for g in range(0, len(clusters_A)): + uv_A = clusters_A[g][0].uv.copy() + uv_B = clusters_B[g][0].uv.copy() + + # localize X values (from symmetry line) + uv_A.x = (uv_A.x - x_middle) + uv_B.x = (uv_B.x - x_middle) + + sortA[g] = abs(uv_A.x) + uv_A.y*2.0 + sortB[g] = abs(uv_B.x) + uv_B.y*2.0 + # print(" . [{}] : {:.2f}, {:.2f} | {:.2f}, {:.2f}".format(g, uv_A.x, uv_A.y, uv_B.x, uv_B.y)) + print(" . [{}] : {:.2f} | {:.2f}".format(g, sortA[g], sortB[g])) + + # Sort sortA by value + sortedA = sorted(sortA.items(), key=operator.itemgetter(1)) + sortedB = sorted(sortB.items(), key=operator.itemgetter(1)) + + for g in range(0, len(clusters_A)): + # sortedA[g] + idxA = sortedA[g][0] + idxB = sortedB[g][0] + + print("Map clusters_A {} -> ".format(idxA, idxB)) + for uv in clusters_B[idxB]: + pos = clusters_A[idxA][0].uv.copy() + # Flip cooreindate + pos.x = x_middle - (pos.x-x_middle) + uv.uv = pos + + + # print("Sorted: '"+str(sortedA)+"'") + # print("Sorted: '"+str(sortedB)+"'") + + # for item in sorted(verts_distance.items(), key=operator.itemgetter(1)): + # connected_verts[i].append( item[0] ) + + # TODO: Now map groups to each other + # uv_avg_A = Vector([0,0]) + # uv_avg_B = Vector([0,0]) + # for m in range(0, len(clusters_A)): + # print(" . ") + # uv_avg_A+= clusters_A[m][0].uv; + # uv_avg_B+= clusters_B[m][0].uv; + + # uv_avg_A/=len(clusters_A) + # uv_avg_B/=len(clusters_B) + + # print(" avg: {} : {}".format(uv_avg_A, uv_avg_B)) + + # Done processing, add to border arrays + border_A.append(vA) + border_B.append(vB) + + ''' + +def alignToCenterLine(): + print("align to center line") + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + + # 1.) Get average edges rotation + center + average_angle = 0 + average_center = Vector((0,0)) + average_count = 0 + for face in bm.faces: + if face.select: + verts = [] + for loop in face.loops: + if loop[uv_layers].select: + verts.append(loop[uv_layers].uv) + + if len(verts) == 2: + diff = verts[1] - verts[0] + angle = math.atan2(diff.y, diff.x)%(math.pi) + average_center += verts[0] + diff/2 + average_angle += angle + average_count+=1 + + if average_count >0: + average_angle/=average_count + average_center/=average_count + + average_angle-= math.pi/2 #Rotate -90 degrees so aligned horizontally + + # 2.) Rotate UV Shell around edge + bpy.context.tool_settings.transform_pivot_point = 'CURSOR' + bpy.ops.uv.cursor_set(location=average_center) + + bpy.ops.uv.select_linked() + bpy.ops.transform.rotate(value=average_angle, orient_axis='Z', constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False) + + + +class UVCluster: + uvs = [] + vertex = None + + def __init__(self, vertex, uvs): + self.vertex = vertex + self.uvs = uvs + + def append(self, uv): + self.uvs.append(uv) + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_island_rotate_90.py b/op_island_rotate_90.py new file mode 100644 index 0000000..1fd5f56 --- /dev/null +++ b/op_island_rotate_90.py @@ -0,0 +1,81 @@ +import bpy +import bmesh +import operator +import math +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv + +class op(bpy.types.Operator): + bl_idname = "uv.textools_island_rotate_90" + bl_label = "Rotate 90 degrees" + bl_description = "Rotate the selected UV island 90 degrees left or right" + bl_options = {'REGISTER', 'UNDO'} + + angle : bpy.props.FloatProperty(name="Angle") + + + @classmethod + def poll(cls, context): + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + # Not in Synced mode + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + + return True + + + def execute(self, context): + + main(context, self.angle) + return {'FINISHED'} + + +def main(context, angle): + + #Store selection + utilities_uv.selection_store() + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + bpy.ops.uv.select_linked() + + #Bounds + bounds_initial = utilities_uv.getSelectionBBox() + bpy.ops.transform.rotate(value=angle, orient_axis='Z', constraint_axis=(False, False, False), use_proportional_edit=False) + + #Align rotation to top left|right + bounds_post = utilities_uv.getSelectionBBox() + dy = bounds_post['max'].y - bounds_initial['max'].y + dx = 0 + if angle > 0: + dx = bounds_post['max'].x - bounds_initial['max'].x + else: + dx = bounds_post['min'].x - bounds_initial['min'].x + bpy.ops.transform.translate(value=(-dx, -dy, 0), constraint_axis=(False, False, False), use_proportional_edit=False) + + + #Restore selection + utilities_uv.selection_restore() + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_island_straighten_edge_loops.py b/op_island_straighten_edge_loops.py new file mode 100644 index 0000000..6b20574 --- /dev/null +++ b/op_island_straighten_edge_loops.py @@ -0,0 +1,272 @@ +import bpy +import bmesh +import operator +import math +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv + +class op(bpy.types.Operator): + bl_idname = "uv.textools_island_straighten_edge_loops" + bl_label = "Straight edge loops" + bl_description = "Straighten edge loops of UV Island and relax rest" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + if bpy.context.scene.tool_settings.uv_select_mode != 'EDGE': + return False + + + return True + + + def execute(self, context): + + main(context) + return {'FINISHED'} + + + + +def main(context): + print("____________________________") + + #Store selection + utilities_uv.selection_store() + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + edges = utilities_uv.get_selected_uv_edges(bm, uv_layers) + islands = utilities_uv.getSelectionIslands() + uvs = utilities_uv.get_selected_uvs(bm, uv_layers) + faces = [f for island in islands for f in island ] + + + # Get island faces + + + # utilities_uv.selection_restore(bm, uv_layers) + + + groups = get_edge_groups(bm, uv_layers, faces, edges, uvs) + + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + bpy.ops.mesh.select_all(action='DESELECT') + for face in faces: + face.select = True + + + print("Edges {}x".format(len(edges))) + print("Groups {}x".format(len(groups))) + + # Restore 3D face selection + + + + + + # Restore UV seams and clear pins + bpy.ops.uv.seams_from_islands() + bpy.ops.uv.pin(clear=True) + + edge_sets = [] + for edges in groups: + edge_sets.append( EdgeSet(bm, uv_layers, edges, faces) ) + # straighten_edges(bm, uv_layers, edges, faces) + + + + + + sorted_sets = sorted(edge_sets, key=lambda x: x.length, reverse=True) + + for edge_set in sorted_sets: + edge_set.straighten() + + #Restore selection + utilities_uv.selection_restore() + + + +class EdgeSet: + bm = None + edges = [] + faces = [] + uv_layers = '' + vert_to_uv = {} + edge_length = {} + length = 0 + + def __init__(self, bm, uv_layers, edges, faces): + self.bm = bm + self.uv_layers = uv_layers + self.edges = edges + self.faces = faces + + # Get Vert to UV within faces + self.vert_to_uv = utilities_uv.get_vert_to_uv(bm, uv_layers) + + # Get edge lengths + self.edge_length = {} + self.length = 0 + for e in edges: + uv1 = self.vert_to_uv[e.verts[0]][0].uv + uv2 = self.vert_to_uv[e.verts[1]][0].uv + self.edge_length[e] = (uv2 - uv1).length + self.length+=self.edge_length[e] + + + def straighten(self): + print("Straight {}x at {:.2f} length ".format(len(self.edges), self.length)) + + # Get edge angles in UV space + angles = {} + for edge in self.edges: + uv1 = self.vert_to_uv[edge.verts[0]][0].uv + uv2 = self.vert_to_uv[edge.verts[1]][0].uv + delta = uv2 - uv1 + angle = math.atan2(delta.y, delta.x)%(math.pi/2) + if angle >= (math.pi/4): + angle = angle - (math.pi/2) + angles[edge] = abs(angle) + # print("Angle {:.2f} degr".format(angle * 180 / math.pi)) + + # Pick edge with least rotation offset to U or V axis + edge_main = sorted(angles.items(), key = operator.itemgetter(1))[0][0] + + print("Main edge: {} at {:.2f} degr".format( edge_main.index, angles[edge_main] * 180 / math.pi )) + + # Rotate main edge to closest axis + uvs = [uv for v in edge_main.verts for uv in self.vert_to_uv[v]] + bpy.ops.uv.select_all(action='DESELECT') + for uv in uvs: + uv.select = True + uv1 = self.vert_to_uv[edge_main.verts[0]][0].uv + uv2 = self.vert_to_uv[edge_main.verts[1]][0].uv + diff = uv2 - uv1 + angle = math.atan2(diff.y, diff.x)%(math.pi/2) + if angle >= (math.pi/4): + angle = angle - (math.pi/2) + bpy.ops.uv.cursor_set(location=uv1 + diff/2) + bpy.ops.transform.rotate(value=angle, orient_axis='Z', constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False) + + # Expand edges and straighten + count = len(self.edges) + processed = [edge_main] + for i in range(count): + if(len(processed) < len(self.edges)): + verts = set([v for e in processed for v in e.verts]) + edges_expand = [e for e in self.edges if e not in processed and (e.verts[0] in verts or e.verts[1] in verts)] + verts_ends = [v for e in edges_expand for v in e.verts if v in verts] + + + print("Step, proc {} exp: {}".format( [e.index for e in processed] , [e.index for e in edges_expand] )) + + if len(edges_expand) == 0: + continue + + for edge in edges_expand: + # if edge.verts[0] in verts_ends and edge.verts[1] in verts_ends: + # print("Cancel at edge {}".format(edge.index)) + # return + + print(" E {} verts {} verts end: {}".format(edge.index, [v.index for v in edge.verts], [v.index for v in verts_ends])) + v1 = [v for v in edge.verts if v in verts_ends][0] + v2 = [v for v in edge.verts if v not in verts_ends][0] + # direction + previous_edge = [e for e in processed if e.verts[0] in edge.verts or e.verts[1] in edge.verts][0] + prev_v1 = [v for v in previous_edge.verts if v != v1][0] + prev_v2 = [v for v in previous_edge.verts if v == v1][0] + direction = (self.vert_to_uv[prev_v2][0].uv - self.vert_to_uv[prev_v1][0].uv).normalized() + + for uv in self.vert_to_uv[v2]: + uv.uv = self.vert_to_uv[v1][0].uv + direction * self.edge_length[edge] + + print("Procesed {}x Expand {}x".format(len(processed), len(edges_expand) )) + print("verts_ends: {}x".format(len(verts_ends))) + processed.extend(edges_expand) + + # Select edges + uvs = list(set( [uv for e in self.edges for v in e.verts for uv in self.vert_to_uv[v] ] )) + bpy.ops.uv.select_all(action='DESELECT') + for uv in uvs: + uv.select = True + + # Pin UV's + bpy.ops.uv.pin() + bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.001) + bpy.ops.uv.pin(clear=True) + + + + + + + + +def get_edge_groups(bm, uv_layers, faces, edges, uvs): + print("Get edge groups, edges {}x".format(len(edges))+"x") + + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + + unmatched = edges.copy() + + groups = [] + + for edge in edges: + if edge in unmatched: + + # Loop select edge + bpy.ops.mesh.select_all(action='DESELECT') + edge.select = True + bpy.ops.mesh.loop_multi_select(ring=False) + + # Isolate group within edges + group = [e for e in bm.edges if e.select and e in edges] + groups.append(group) + + # Remove from unmatched + for e in group: + if e in unmatched: + unmatched.remove(e) + + print(" Edge {} : Group: {}x , unmatched: {}".format(edge.index, len(group), len(unmatched))) + + # return + # group = [edge] + # for e in bm.edges: + # if e.select and e in unmatched: + # unmatched.remove(e) + # group.append(edge) + + + + return groups + + +bpy.utils.register_class(op) + + diff --git a/op_meshtex_create.py b/op_meshtex_create.py new file mode 100644 index 0000000..6a2b70b --- /dev/null +++ b/op_meshtex_create.py @@ -0,0 +1,296 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi +import math +from . import utilities_uv +from . import utilities_texel +from . import utilities_meshtex + + +def get_mode(): + if not utilities_meshtex.find_uv_mesh([bpy.context.active_object]): + # Create UV mesh from face selection + if bpy.context.active_object and bpy.context.active_object.mode == 'EDIT': + return 'FACES' + + # Create UV mesh from whole object + if bpy.context.active_object and bpy.context.active_object.type == 'MESH': + if "SurfaceDeform" not in bpy.context.active_object.modifiers: + return 'OBJECT' + + return 'UNDEFINED' + + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_meshtex_create" + bl_label = "UV Mesh" + bl_description = "Create a new UV Mesh from your selected object" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if get_mode() == 'UNDEFINED': + return False + return True + + + def execute(self, context): + create_uv_mesh(self, bpy.context.active_object) + return {'FINISHED'} + + + +def create_uv_mesh(self, obj): + + mode = bpy.context.active_object.mode + + # Select + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type="FACE") + bpy.context.scene.tool_settings.use_uv_select_sync = False + + + # Select all if OBJECT mode + if mode == 'OBJECT': + bpy.ops.mesh.select_all(action='SELECT') + # bpy.ops.uv.select_all(action='SELECT') + + # Create UV Map + if not obj.data.uv_layers: + if mode == 'OBJECT': + # Smart UV project + bpy.ops.uv.smart_project( + angle_limit=65, + island_margin=0.5, + user_area_weight=0, + use_aspect=True, + stretch_to_bounds=True + ) + elif mode == 'EDIT': + # Iron Faces + bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0) + bpy.ops.uv.textools_unwrap_faces_iron() + + + bm = bmesh.from_edit_mesh(obj.data) + uv_layers = bm.loops.layers.uv.verify() + + #Collect UV islands + bpy.ops.uv.select_all(action='SELECT') + islands = utilities_uv.getSelectionIslands(bm, uv_layers) + + # Collect clusters + uvs = {} + clusters = [] + uv_to_clusters = {} + vert_to_clusters = {} + + face_area_view = 0 + face_area_uv = 0 + + for face in bm.faces: + if face.select: + # Calculate triangle area for UV and View + # Triangle Verts + tri_uv = [loop[uv_layers].uv for loop in face.loops ] + tri_vt = [vert.co for vert in face.verts] + + #Triangle Areas + face_area_view += math.sqrt(utilities_texel.get_area_triangle( + tri_vt[0], + tri_vt[1], + tri_vt[2] + )) + face_area_uv += math.sqrt(utilities_texel.get_area_triangle( + tri_uv[0], + tri_uv[1], + tri_uv[2] + )) + + for i in range(len(face.loops)): + v = face.loops[i] + uv = Get_UVSet(uvs, bm, uv_layers, face.index, i) + + # # clusters + isMerged = False + for cluster in clusters: + d = (uv.pos() - cluster.uvs[0].pos()).length + if d <= 0.0000001: + #Merge + cluster.append(uv) + uv_to_clusters[uv] = cluster + if v not in vert_to_clusters: + vert_to_clusters[v] = cluster + isMerged = True; + break; + if not isMerged: + #New Group + clusters.append( UVCluster(v, [uv]) ) + uv_to_clusters[uv] = clusters[-1] + if v not in vert_to_clusters: + vert_to_clusters[v] = clusters[-1] + + scale = face_area_view / face_area_uv + + print("Scale {}x {} | {}".format(scale, face_area_view, face_area_uv)) + print("Islands {}x".format(len(islands))) + print("UV Vert Clusters {}x".format(len(clusters))) + + m_vert_cluster = [] + m_verts_org = [] + m_verts_A = [] + m_verts_B = [] + m_faces = [] + + for island in islands: + for face in island: + f = [] + for i in range(len(face.loops)): + v = face.loops[i].vert + uv = Get_UVSet(uvs, bm, uv_layers, face.index, i) + c = uv_to_clusters[ uv ] + + index = 0 + if c in m_vert_cluster: + index = m_vert_cluster.index(c) + + else: + index = len(m_vert_cluster) + m_vert_cluster.append(c) + m_verts_org.append(v) + + m_verts_A.append( Vector((uv.pos().x*scale - scale/2, uv.pos().y*scale -scale/2, 0)) ) + m_verts_B.append( obj.matrix_world @ v.co - bpy.context.scene.cursor.location ) + + f.append(index) + + m_faces.append(f) + + # Add UV bounds as edges + verts = [ + Vector((-scale/2, -scale/2, 0)), + Vector(( scale/2, -scale/2, 0)), + Vector(( scale/2, scale/2, 0)), + Vector((-scale/2, scale/2, 0)), + ] + m_verts_A = m_verts_A+verts; + m_verts_B = m_verts_B+verts; + + bpy.ops.object.mode_set(mode='OBJECT') + + # Create Mesh + mesh = bpy.data.meshes.new("mesh_texture") + mesh.from_pydata(m_verts_A, [], m_faces) + mesh.update() + mesh_obj = bpy.data.objects.new("UV_mesh {0}".format(obj.name), mesh) + mesh_obj.location = bpy.context.scene.cursor.location + bpy.context.collection.objects.link(mesh_obj) + + # Add shape keys + mesh_obj.shape_key_add(name="uv", from_mix=True) + mesh_obj.shape_key_add(name="model", from_mix=True) + mesh_obj.active_shape_key_index = 1 + + # Select + bpy.context.view_layer.objects.active = mesh_obj + mesh_obj.select_set( state = True, view_layer = None) + + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(mesh_obj.data) + + if hasattr(bm.faces, "ensure_lookup_table"): + bm.faces.ensure_lookup_table() + bm.verts.ensure_lookup_table() + + bm.edges.new((bm.verts[-4], bm.verts[-3])) + bm.edges.new((bm.verts[-3], bm.verts[-2])) + bm.edges.new((bm.verts[-2], bm.verts[-1])) + bm.edges.new((bm.verts[-1], bm.verts[-4])) + + + for i in range(len(m_verts_B)): + bm.verts[i].co = m_verts_B[i] + + + # Split concave faces to resolve issues with Shape deform + bpy.context.object.active_shape_key_index = 0 + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.vert_connect_concave() + + + bpy.ops.object.mode_set(mode='OBJECT') + + + # Display as edges only + mesh_obj.show_wire = True + mesh_obj.show_all_edges = True + # mesh_obj.data.display_type = 'WIRE' #Esta linea deberia llevarte a la opcion wireframe + + + + bpy.ops.object.select_all(action='DESELECT') + mesh_obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = mesh_obj + + + +def Get_UVSet(uvs, bm, layer, index_face, index_loop): + index = get_uv_index(index_face, index_loop) + if index not in uvs: + uvs[index] = UVSet(bm, layer, index_face, index_loop) + + return uvs[index] + + + +class UVSet: + bm = None + layer = None + index_face = 0 + index_loop = 0 + + def __init__(self, bm, layer, index_face, index_loop): + self.bm = bm + self.layer = layer + self.index_face = index_face + self.index_loop = index_loop + + def uv(self): + face = self.bm.faces[self.index_face] + return face.loops[self.index_loop][self.layer] + + def pos(self): + return self.uv().uv + + def vertex(self): + return face.loops[self.index_loop].vertex + + + +def get_uv_index(index_face, index_loop): + return (index_face*1000000)+index_loop + + +class UVCluster: + uvs = [] + vertex = None + + def __init__(self, vertex, uvs): + self.vertex = vertex + self.uvs = uvs + + def append(self, uv): + self.uvs.append(uv) + +bpy.utils.register_class(op) diff --git a/op_meshtex_pattern.py b/op_meshtex_pattern.py new file mode 100644 index 0000000..40630c1 --- /dev/null +++ b/op_meshtex_pattern.py @@ -0,0 +1,193 @@ +import bpy +import bmesh +import operator + +from mathutils import Vector +from collections import defaultdict +from math import pi +import math + +from . import utilities_meshtex +from . import utilities_ui + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_meshtex_pattern" + bl_label = "Create Pattern" + bl_description = "Create mesh pattern" + bl_options = {'REGISTER', 'UNDO'} + + mode : bpy.props.EnumProperty(items= + [('hexagon', 'Hexagons', ''), + ('triangle', 'Triangles', ''), + ('diamond', 'Diamonds', ''), + ('rectangle', 'Rectangles', ''), + ('stripe', 'Stripes', ''), + ('brick', 'Bricks', '')], + name = "Mode", + default = 'brick' + ) + + size : bpy.props.IntProperty( + name = "Size", + description = "Size X and Y of the repetition", + default = 4, + min = 1, + max = 128 + ) + + scale : bpy.props.FloatProperty( + name = "Scale", + description = "Scale of the mesh pattern", + default = 1, + min = 0 + ) + + @classmethod + def poll(cls, context): + + if bpy.context.active_object and bpy.context.active_object.mode != 'OBJECT': + return False + + return True + + def draw(self, context): + layout = self.layout + layout.prop(self, "mode") + layout.prop(self, "size") + layout.prop(self, "scale") + + def execute(self, context): + create_pattern(self, self.mode, self.size, self.scale) + return {'FINISHED'} + + + + + +def AddArray(name, offset_x, offset_y, count): + modifier = bpy.context.object.modifiers.new(name=name, type='ARRAY') + # modifier = bpy.context.object.modifiers.new(name="{}_{}".format(name,count), type='ARRAY') + modifier.relative_offset_displace[0] = offset_x + modifier.relative_offset_displace[1] = offset_y + modifier.count = count + modifier.show_expanded = False + return modifier + + + + + +def create_pattern(self, mode, size, scale): + + print("Create pattern {}".format(mode)) + + # bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + + context_override = None + if bpy.context.area.type != 'VIEW_3D': + context_override = utilities_ui.GetContextView3D() + if not context_override: + self.report({'ERROR_INVALID_INPUT'}, "This tool requires an available View3D view.") + return + + print("Mode '{}' size: '{}'".format(mode, size)) + + + if mode == 'hexagon': + bpy.ops.mesh.primitive_circle_add(vertices=6, size=scale, fill_type='NGON') + + bpy.ops.object.mode_set(mode = 'EDIT') + if context_override: + bpy.ops.transform.rotate(context_override, value=math.pi*0.5, orient_axis='Z') + else: + bpy.ops.transform.rotate(value=math.pi*0.5, orient_axis='Z') + + bpy.ops.object.mode_set(mode = 'OBJECT') + + AddArray("Array0", 0.75,-0.5,2) + AddArray("Array1", 0,-0.66666666666,size) + AddArray("Array2", 1 - (0.5/3.5),0,size*0.66) + + elif mode == 'triangle': + bpy.ops.mesh.primitive_circle_add(vertices=3, size=scale, fill_type='NGON') + + bpy.ops.object.mode_set(mode = 'EDIT') + + if context_override: + bpy.ops.transform.translate(context_override, value=(0, scale*0.5, 0), constraint_axis=(False, True, False)) + else: + bpy.ops.transform.translate(value=(0, scale*0.5, 0), constraint_axis=(False, True, False)) + + bpy.ops.object.mode_set(mode = 'OBJECT') + + modifier = bpy.context.object.modifiers.new(name="Mirror", type='MIRROR') + modifier.use_y = True + modifier.use_x = False + modifier.show_expanded = False + AddArray("Array0", 0.5,-0.5,2) + AddArray("Array1", 1-1/3.0,0,size) + AddArray("Array1", 0,-(1-1/3.0),size*0.66) + + elif mode == 'rectangle': + bpy.ops.mesh.primitive_plane_add(size=scale) + AddArray("Array0", 1,0,size) + AddArray("Array1", 0,-1,size) + + elif mode == 'diamond': + bpy.ops.mesh.primitive_plane_add(size=scale) + + bpy.ops.object.mode_set(mode = 'EDIT') + + if context_override: + bpy.ops.transform.rotate(context_override, value=math.pi*0.25, orient_axis='Z') + else: + bpy.ops.transform.rotate(value=math.pi*0.25, orient_axis='Z') + + bpy.ops.object.mode_set(mode = 'OBJECT') + + AddArray("Array0", 0.5,-0.5,2) + AddArray("Array1", 1-1/3,0,size) + AddArray("Array2", 0,-(1-1/3),size) + + elif mode == 'brick': + bpy.ops.mesh.primitive_plane_add(size=scale) + + bpy.ops.object.mode_set(mode = 'EDIT') + + if context_override: + bpy.ops.transform.resize(context_override, value=(1, 0.5, 1), constraint_axis=(True, True, False), orient_type='GLOBAL') + else: + bpy.ops.transform.resize(value=(1, 0.5, 1), constraint_axis=(True, True, False), orient_type='GLOBAL') + + bpy.ops.object.mode_set(mode = 'OBJECT') + + + AddArray("Array0", 0.5,-1,2) + AddArray("Array1", 1-(1/3),0,size) + AddArray("Array2", 0,-1,size) + + + elif mode == 'stripe': + bpy.ops.mesh.primitive_plane_add(size=1) + + bpy.ops.object.mode_set(mode = 'EDIT') + if context_override: + bpy.ops.transform.resize(context_override, value=(0.5, size/2, 1), constraint_axis=(True, True, False), orient_type='GLOBAL') + bpy.ops.transform.resize(context_override, value=(scale, scale, 1), constraint_axis=(True, True, False), orient_type='GLOBAL') + bpy.ops.transform.translate(context_override, value=(0, (-size/2)*scale, 0), constraint_axis=(False, True, False)) + else: + bpy.ops.transform.resize(value=(0.5, size/2, 1), constraint_axis=(True, True, False), orient_type='GLOBAL') + bpy.ops.transform.resize(value=(scale, scale, 1), constraint_axis=(True, True, False), orient_type='GLOBAL') + bpy.ops.transform.translate(value=(0, (-size/2)*scale, 0), constraint_axis=(False, True, False)) + + bpy.ops.object.mode_set(mode = 'OBJECT') + + AddArray("Array0", 1,0, size) + + # if bpy.context.object: + # bpy.context.object.name = "pattern_{}".format(mode) + # bpy.context.object.show_wire = True + +bpy.utils.register_class(op) + diff --git a/op_meshtex_trim.py b/op_meshtex_trim.py new file mode 100644 index 0000000..143c32e --- /dev/null +++ b/op_meshtex_trim.py @@ -0,0 +1,77 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi +import math + +from . import utilities_meshtex + + + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_meshtex_trim" + bl_label = "Trim" + bl_description = "Trim Mesh Texture" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object or bpy.context.active_object.mode != 'OBJECT': + return False + + if len(bpy.context.selected_objects) >= 1: + # Find a UV mesh + if utilities_meshtex.find_uv_mesh(bpy.context.selected_objects): + # Find 1 or more meshes to wrap + if len( utilities_meshtex.find_texture_meshes(bpy.context.selected_objects)) > 0: + return True + + return False + + def execute(self, context): + trim(self) + return {'FINISHED'} + + + +def trim(self): + # Wrap the mesh texture around the + print("Trim Mesh Texture :)") + + # Collect UV mesh + obj_uv = utilities_meshtex.find_uv_mesh(bpy.context.selected_objects) + if not obj_uv: + self.report({'ERROR_INVALID_INPUT'}, "No UV mesh found" ) + return + + # Collect texture meshes + obj_textures = utilities_meshtex.find_texture_meshes( bpy.context.selected_objects ) + + + if len(obj_textures) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No meshes found for mesh textures" ) + return + + # Setup Thickness + utilities_meshtex.uv_mesh_fit(obj_uv, obj_textures) + + + # Apply bool modifier to trim + for obj in obj_textures: + name = "Trim UV" + + if name in obj.modifiers: + obj.modifiers.remove( obj.modifiers[name] ) + + modifier_bool = obj.modifiers.new(name=name, type='BOOLEAN') + modifier_bool.object = obj_uv + + bpy.ops.ui.textools_popup('INVOKE_DEFAULT', message="Collapse modifiers before wrapping") + + +bpy.utils.register_class(op) + + diff --git a/op_meshtex_trim_collapse.py b/op_meshtex_trim_collapse.py new file mode 100644 index 0000000..ae18418 --- /dev/null +++ b/op_meshtex_trim_collapse.py @@ -0,0 +1,70 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi +import math + +from . import utilities_meshtex + + +def is_available(): + # If the selection contains a boolean modifier + obj_textures = utilities_meshtex.find_texture_meshes(bpy.context.selected_objects) + for obj in obj_textures: + for modifier in obj.modifiers: + if modifier.type == 'BOOLEAN': + return True + return False + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_meshtex_trimcollapse" + bl_label = "Collapse" + bl_description = "Trim Mesh Texture" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object or bpy.context.active_object.mode != 'OBJECT': + return False + + return is_available() + + def execute(self, context): + collapse(self) + return {'FINISHED'} + + + +def collapse(self): + # Collect texture meshes + obj_textures = utilities_meshtex.find_texture_meshes( bpy.context.selected_objects ) + + + previous_selection = bpy.context.selected_objects.copy() + previous_active = bpy.context.view_layer.objects.active + + if len(obj_textures) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No meshes found for mesh textures" ) + return + + # Apply bool modifier to trim + for obj in obj_textures: + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.convert(target='MESH') + + # restore selection + bpy.ops.object.select_all(action='DESELECT') + for obj in previous_selection: + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = previous_active + + + bpy.ops.ui.textools_popup('INVOKE_DEFAULT', message="{}x objects have been collapsed".format(len(obj_textures))) + + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_meshtex_wrap.py b/op_meshtex_wrap.py new file mode 100644 index 0000000..523e4f8 --- /dev/null +++ b/op_meshtex_wrap.py @@ -0,0 +1,86 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi +import math +from . import utilities_meshtex + + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_meshtex_wrap" + bl_label = "Wrap Mesh Texture" + bl_description = "Swap UV to XYZ coordinates" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object or bpy.context.active_object.mode != 'OBJECT': + return False + + # Wrap texture mesh around UV mesh + if len(bpy.context.selected_objects) >= 1: + # Find a UV mesh + if utilities_meshtex.find_uv_mesh(bpy.context.selected_objects): + # Find 1 or more meshes to wrap + if len( utilities_meshtex.find_texture_meshes(bpy.context.selected_objects)) > 0: + return True + + return False + + def execute(self, context): + wrap_meshtex(self) + return {'FINISHED'} + + + +def wrap_meshtex(self): + # Wrap the mesh texture around the + print("Wrap Mesh Texture :)") + + # Collect UV mesh + obj_uv = utilities_meshtex.find_uv_mesh(bpy.context.selected_objects) + if not obj_uv: + self.report({'ERROR_INVALID_INPUT'}, "No UV mesh found" ) + return + + # Collect texture meshes + obj_textures = utilities_meshtex.find_texture_meshes( bpy.context.selected_objects ) + + if len(obj_textures) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No meshes found for mesh textures" ) + return + + print("Wrap {} texture meshes".format(len(obj_textures))) + + # Undo wrapping + if bpy.context.scene.texToolsSettings.meshtexture_wrap > 0: + bpy.context.scene.texToolsSettings.meshtexture_wrap = 0 + # Clear modifiers + utilities_meshtex.uv_mesh_clear(obj_uv) + return + + # Setup Thickness + utilities_meshtex.uv_mesh_fit(obj_uv, obj_textures) + + for obj in obj_textures: + # Delete previous modifiers + for modifier in obj.modifiers: + if modifier.type == 'SURFACE_DEFORM': + obj.modifiers.remove(modifier) + break + + # Add mesh modifier + modifier_deform = obj.modifiers.new(name="SurfaceDeform", type='SURFACE_DEFORM') + modifier_deform.target = obj_uv + + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.surfacedeform_bind(modifier="SurfaceDeform") + + # Apply wrapped morph state + bpy.context.scene.texToolsSettings.meshtexture_wrap = 1 + +bpy.utils.register_class(op) diff --git a/op_rectify.py b/op_rectify.py new file mode 100644 index 0000000..e5fc90a --- /dev/null +++ b/op_rectify.py @@ -0,0 +1,637 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi +import time +from math import radians, hypot + +from . import utilities_uv + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_rectify" + bl_label = "Rectify" + bl_description = "Align selected faces or verts to rectangular distribution." + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + if bpy.context.active_object.mode != 'EDIT': + return False + + # No Sync mode + if context.scene.tool_settings.use_uv_select_sync: + return False + + return True + + + def execute(self, context): + rectify(self, context) + return {'FINISHED'} + + + +precision = 3 + + +def rectify(self, context): + obj = bpy.context.active_object + + + bm = bmesh.from_edit_mesh(obj.data) + uv_layers = bm.loops.layers.uv.verify() + + #Store selection + utilities_uv.selection_store() + + main(False) + + #Restore selection + utilities_uv.selection_restore() + + +def main(square = False, snapToClosest = False): + + startTime = time.clock() + obj = bpy.context.active_object + me = obj.data + bm = bmesh.from_edit_mesh(me) + uv_layers = bm.loops.layers.uv.verify() + # bm.faces.layers.tex.verify() # currently blender needs both layers. + + face_act = bm.faces.active + targetFace = face_act + + #if len(bm.faces) > allowedFaces: + # operator.report({'ERROR'}, "selected more than " +str(allowedFaces) +" allowed faces.") + # return + + edgeVerts, filteredVerts, selFaces, nonQuadFaces, vertsDict, noEdge = ListsOfVerts(uv_layers, bm) + + if len(filteredVerts) is 0: return + if len(filteredVerts) is 1: + SnapCursorToClosestSelected(filteredVerts) + return + + cursorClosestTo = CursorClosestTo(filteredVerts) + #line is selected + + if len(selFaces) is 0: + if snapToClosest is True: + SnapCursorToClosestSelected(filteredVerts) + return + + VertsDictForLine(uv_layers, bm, filteredVerts, vertsDict) + + if AreVectsLinedOnAxis(filteredVerts) is False: + ScaleTo0OnAxisAndCursor(filteredVerts, vertsDict, cursorClosestTo) + return SuccessFinished(me, startTime) + + MakeEqualDistanceBetweenVertsInLine(filteredVerts, vertsDict, cursorClosestTo) + return SuccessFinished(me, startTime) + + + #else: + + #active face checks + if targetFace is None or targetFace.select is False or len(targetFace.verts) is not 4: + targetFace = selFaces[0] + else: + for l in targetFace.loops: + if l[uv_layers].select is False: + targetFace = selFaces[0] + break + + ShapeFace(uv_layers, operator, targetFace, vertsDict, square) + + for nf in nonQuadFaces: + for l in nf.loops: + luv = l[uv_layers] + luv.select = False + + if square: FollowActiveUV(operator, me, targetFace, selFaces, 'EVEN') + else: FollowActiveUV(operator, me, targetFace, selFaces) + + if noEdge is False: + #edge has ripped so we connect it back + for ev in edgeVerts: + key = (round(ev.uv.x, precision), round(ev.uv.y, precision)) + if key in vertsDict: + ev.uv = vertsDict[key][0].uv + ev.select = True + + return SuccessFinished(me, startTime) + + + +def ListsOfVerts(uv_layers, bm): + edgeVerts = [] + allEdgeVerts = [] + filteredVerts = [] + selFaces = [] + nonQuadFaces = [] + vertsDict = defaultdict(list) #dict + + for f in bm.faces: + isFaceSel = True + facesEdgeVerts = [] + if (f.select == False): + continue + + #collect edge verts if any + for l in f.loops: + luv = l[uv_layers] + if luv.select is True: + facesEdgeVerts.append(luv) + else: isFaceSel = False + + allEdgeVerts.extend(facesEdgeVerts) + if isFaceSel: + if len(f.verts) is not 4: + nonQuadFaces.append(f) + edgeVerts.extend(facesEdgeVerts) + else: + selFaces.append(f) + + for l in f.loops: + luv = l[uv_layers] + x = round(luv.uv.x, precision) + y = round(luv.uv.y, precision) + vertsDict[(x, y)].append(luv) + + else: edgeVerts.extend(facesEdgeVerts) + + noEdge = False + if len(edgeVerts) is 0: + noEdge = True + edgeVerts.extend(allEdgeVerts) + + if len(selFaces) is 0: + for ev in edgeVerts: + if ListQuasiContainsVect(filteredVerts, ev) is False: + filteredVerts.append(ev) + else: filteredVerts = edgeVerts + + return edgeVerts, filteredVerts, selFaces, nonQuadFaces, vertsDict, noEdge + + + +def ListQuasiContainsVect(list, vect): + for v in list: + if AreVertsQuasiEqual(v, vect): + return True + return False + + + +def SnapCursorToClosestSelected(filteredVerts): + #TODO: snap to closest selected + if len(filteredVerts) is 1: + SetAll2dCursorsTo(filteredVerts[0].uv.x, filteredVerts[0].uv.y) + + return + + + + + +def VertsDictForLine(uv_layers, bm, selVerts, vertsDict): + for f in bm.faces: + for l in f.loops: + luv = l[uv_layers] + if luv.select is True: + x = round(luv.uv.x, precision) + y = round(luv.uv.y, precision) + + vertsDict[(x, y)].append(luv) + + + +def AreVectsLinedOnAxis(verts): + areLinedX = True + areLinedY = True + allowedError = 0.0009 + valX = verts[0].uv.x + valY = verts[0].uv.y + for v in verts: + if abs(valX - v.uv.x) > allowedError: + areLinedX = False + if abs(valY - v.uv.y) > allowedError: + areLinedY = False + return areLinedX or areLinedY + + + +def ScaleTo0OnAxisAndCursor(filteredVerts, vertsDict, startv = None, horizontal = None): + + verts = filteredVerts + verts.sort(key=lambda x: x.uv[0]) #sort by .x + + first = verts[0] + last = verts[len(verts)-1] + + if horizontal is None: + horizontal = True + if ((last.uv.x - first.uv.x) >0.0009): + slope = (last.uv.y - first.uv.y)/(last.uv.x - first.uv.x) + if (slope > 1) or (slope <-1): + horizontal = False + else: + horizontal = False + + if horizontal is True: + if startv is None: + startv = first + + SetAll2dCursorsTo(startv.uv.x, startv.uv.y) + #scale to 0 on Y + ScaleTo0('Y') + return + + else: + verts.sort(key=lambda x: x.uv[1]) #sort by .y + verts.reverse() #reverse because y values drop from up to down + first = verts[0] + last = verts[len(verts)-1] + if startv is None: + startv = first + + SetAll2dCursorsTo(startv.uv.x, startv.uv.y) + #scale to 0 on X + ScaleTo0('X') + return + + + +def SetAll2dCursorsTo(x,y): + last_area = bpy.context.area.type + bpy.context.area.type = 'IMAGE_EDITOR' + + bpy.ops.uv.cursor_set(location=(x, y)) + + bpy.context.area.type = last_area + return + + + +def CursorClosestTo(verts, allowedError = 0.025): + ratioX, ratioY = ImageRatio() + + #any length that is certantly not smaller than distance of the closest + min = 1000 + minV = verts[0] + for v in verts: + if v is None: continue + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + loc = area.spaces[0].cursor_location + hyp = hypot(loc.x/ratioX -v.uv.x, loc.y/ratioY -v.uv.y) + if (hyp < min): + min = hyp + minV = v + + if min is not 1000: + return minV + return None + + + + +def SuccessFinished(me, startTime): + #use for backtrack of steps + #bpy.ops.ed.undo_push() + bmesh.update_edit_mesh(me) + #elapsed = round(time.clock()-startTime, 2) + #if (elapsed >= 0.05): operator.report({'INFO'}, "UvSquares finished, elapsed:", elapsed, "s.") + return + + + +def ShapeFace(uv_layers, operator, targetFace, vertsDict, square): + corners = [] + for l in targetFace.loops: + luv = l[uv_layers] + corners.append(luv) + + if len(corners) is not 4: + #operator.report({'ERROR'}, "bla") + return + + lucv, ldcv, rucv, rdcv = Corners(corners) + + cct = CursorClosestTo([lucv, ldcv, rdcv, rucv]) + if cct is None: + cct = lucv + + MakeUvFaceEqualRectangle(vertsDict, lucv, rucv, rdcv, ldcv, cct, square) + return + + + +def MakeUvFaceEqualRectangle(vertsDict, lucv, rucv, rdcv, ldcv, startv, square = False): + ratioX, ratioY = ImageRatio() + ratio = ratioX/ratioY + + if startv is None: startv = lucv.uv + elif AreVertsQuasiEqual(startv, rucv): startv = rucv.uv + elif AreVertsQuasiEqual(startv, rdcv): startv = rdcv.uv + elif AreVertsQuasiEqual(startv, ldcv): startv = ldcv.uv + else: startv = lucv.uv + + lucv = lucv.uv + rucv = rucv.uv + rdcv = rdcv.uv + ldcv = ldcv.uv + + if (startv == lucv): + finalScaleX = hypotVert(lucv, rucv) + finalScaleY = hypotVert(lucv, ldcv) + currRowX = lucv.x + currRowY = lucv.y + + elif (startv == rucv): + finalScaleX = hypotVert(rucv, lucv) + finalScaleY = hypotVert(rucv, rdcv) + currRowX = rucv.x - finalScaleX + currRowY = rucv.y + + elif (startv == rdcv): + finalScaleX = hypotVert(rdcv, ldcv) + finalScaleY = hypotVert(rdcv, rucv) + currRowX = rdcv.x - finalScaleX + currRowY = rdcv.y + finalScaleY + + else: + finalScaleX = hypotVert(ldcv, rdcv) + finalScaleY = hypotVert(ldcv, lucv) + currRowX = ldcv.x + currRowY = ldcv.y +finalScaleY + + if square: finalScaleY = finalScaleX*ratio + #lucv, rucv + x = round(lucv.x, precision) + y = round(lucv.y, precision) + for v in vertsDict[(x,y)]: + v.uv.x = currRowX + v.uv.y = currRowY + + x = round(rucv.x, precision) + y = round(rucv.y, precision) + for v in vertsDict[(x,y)]: + v.uv.x = currRowX + finalScaleX + v.uv.y = currRowY + + #rdcv, ldcv + x = round(rdcv.x, precision) + y = round(rdcv.y, precision) + for v in vertsDict[(x,y)]: + v.uv.x = currRowX + finalScaleX + v.uv.y = currRowY - finalScaleY + + x = round(ldcv.x, precision) + y = round(ldcv.y, precision) + for v in vertsDict[(x,y)]: + v.uv.x = currRowX + v.uv.y = currRowY - finalScaleY + + + return + + + +def FollowActiveUV(operator, me, f_act, faces, EXTEND_MODE = 'LENGTH_AVERAGE'): + bm = bmesh.from_edit_mesh(me) + uv_act = bm.loops.layers.uv.active + + # our own local walker + def walk_face_init(faces, f_act): + # first tag all faces True (so we dont uvmap them) + for f in bm.faces: + f.tag = True + # then tag faces arg False + for f in faces: + f.tag = False + # tag the active face True since we begin there + f_act.tag = True + + def walk_face(f): + # all faces in this list must be tagged + f.tag = True + faces_a = [f] + faces_b = [] + + while faces_a: + for f in faces_a: + for l in f.loops: + l_edge = l.edge + if (l_edge.is_manifold is True) and (l_edge.seam is False): + l_other = l.link_loop_radial_next + f_other = l_other.face + if not f_other.tag: + yield (f, l, f_other) + f_other.tag = True + faces_b.append(f_other) + # swap + faces_a, faces_b = faces_b, faces_a + faces_b.clear() + + def walk_edgeloop(l): + """ + Could make this a generic function + """ + e_first = l.edge + e = None + while True: + e = l.edge + yield e + + # don't step past non-manifold edges + if e.is_manifold: + # welk around the quad and then onto the next face + l = l.link_loop_radial_next + if len(l.face.verts) == 4: + l = l.link_loop_next.link_loop_next + if l.edge is e_first: + break + else: + break + else: + break + + def extrapolate_uv(fac, + l_a_outer, l_a_inner, + l_b_outer, l_b_inner): + l_b_inner[:] = l_a_inner + l_b_outer[:] = l_a_inner + ((l_a_inner - l_a_outer) * fac) + + def apply_uv(f_prev, l_prev, f_next): + l_a = [None, None, None, None] + l_b = [None, None, None, None] + + l_a[0] = l_prev + l_a[1] = l_a[0].link_loop_next + l_a[2] = l_a[1].link_loop_next + l_a[3] = l_a[2].link_loop_next + + # l_b + # +-----------+ + # |(3) |(2) + # | | + # |l_next(0) |(1) + # +-----------+ + # ^ + # l_a | + # +-----------+ + # |l_prev(0) |(1) + # | (f) | + # |(3) |(2) + # +-----------+ + # copy from this face to the one above. + + # get the other loops + l_next = l_prev.link_loop_radial_next + if l_next.vert != l_prev.vert: + l_b[1] = l_next + l_b[0] = l_b[1].link_loop_next + l_b[3] = l_b[0].link_loop_next + l_b[2] = l_b[3].link_loop_next + else: + l_b[0] = l_next + l_b[1] = l_b[0].link_loop_next + l_b[2] = l_b[1].link_loop_next + l_b[3] = l_b[2].link_loop_next + + l_a_uv = [l[uv_act].uv for l in l_a] + l_b_uv = [l[uv_act].uv for l in l_b] + + if EXTEND_MODE == 'LENGTH_AVERAGE': + fac = edge_lengths[l_b[2].edge.index][0] / edge_lengths[l_a[1].edge.index][0] + elif EXTEND_MODE == 'LENGTH': + a0, b0, c0 = l_a[3].vert.co, l_a[0].vert.co, l_b[3].vert.co + a1, b1, c1 = l_a[2].vert.co, l_a[1].vert.co, l_b[2].vert.co + + d1 = (a0 - b0).length + (a1 - b1).length + d2 = (b0 - c0).length + (b1 - c1).length + try: + fac = d2 / d1 + except ZeroDivisionError: + fac = 1.0 + else: + fac = 1.0 + + extrapolate_uv(fac, + l_a_uv[3], l_a_uv[0], + l_b_uv[3], l_b_uv[0]) + + extrapolate_uv(fac, + l_a_uv[2], l_a_uv[1], + l_b_uv[2], l_b_uv[1]) + + # ------------------------------------------- + # Calculate average length per loop if needed + + if EXTEND_MODE == 'LENGTH_AVERAGE': + bm.edges.index_update() + edge_lengths = [None] * len(bm.edges) #NoneType times the length of edges list + + for f in faces: + # we know its a quad + l_quad = f.loops[:] + l_pair_a = (l_quad[0], l_quad[2]) + l_pair_b = (l_quad[1], l_quad[3]) + + for l_pair in (l_pair_a, l_pair_b): + if edge_lengths[l_pair[0].edge.index] is None: + + edge_length_store = [-1.0] + edge_length_accum = 0.0 + edge_length_total = 0 + + for l in l_pair: + if edge_lengths[l.edge.index] is None: + for e in walk_edgeloop(l): + if edge_lengths[e.index] is None: + edge_lengths[e.index] = edge_length_store + edge_length_accum += e.calc_length() + edge_length_total += 1 + + edge_length_store[0] = edge_length_accum / edge_length_total + + # done with average length + # ------------------------ + + walk_face_init(faces, f_act) + for f_triple in walk_face(f_act): + apply_uv(*f_triple) + + bmesh.update_edit_mesh(me, False) + + +def ImageRatio(): + ratioX, ratioY = 256,256 + for a in bpy.context.screen.areas: + if a.type == 'IMAGE_EDITOR': + img = a.spaces[0].image + if img is not None and img.size[0] is not 0: + ratioX, ratioY = img.size[0], img.size[1] + break + return ratioX, ratioY + + + +def Corners(corners): + firstHighest = corners[0] + for c in corners: + if c.uv.y > firstHighest.uv.y: + firstHighest = c + corners.remove(firstHighest) + + secondHighest = corners[0] + for c in corners: + if (c.uv.y > secondHighest.uv.y): + secondHighest = c + + if firstHighest.uv.x < secondHighest.uv.x: + leftUp = firstHighest + rightUp = secondHighest + else: + leftUp = secondHighest + rightUp = firstHighest + corners.remove(secondHighest) + + firstLowest = corners[0] + secondLowest = corners[1] + + if firstLowest.uv.x < secondLowest.uv.x: + leftDown = firstLowest + rightDown = secondLowest + else: + leftDown = secondLowest + rightDown = firstLowest + + return leftUp, leftDown, rightUp, rightDown + + + + +def AreVertsQuasiEqual(v1, v2, allowedError = 0.0009): + if abs(v1.uv.x -v2.uv.x) < allowedError and abs(v1.uv.y -v2.uv.y) < allowedError: + return True + return False + + + +def hypotVert(v1, v2): + hyp = hypot(v1.x - v2.x, v1.y - v2.y) + return hyp + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_select_islands_flipped.py b/op_select_islands_flipped.py new file mode 100644 index 0000000..7ff38d5 --- /dev/null +++ b/op_select_islands_flipped.py @@ -0,0 +1,133 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv +import imp +imp.reload(utilities_uv) + +class op(bpy.types.Operator): + bl_idname = "uv.textools_select_islands_flipped" + bl_label = "Select Flipped" + bl_description = "Select all flipped UV islands" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + ##Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + #Not in Synced mode + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + + return True + + + def execute(self, context): + + select_flipped(context) + return {'FINISHED'} + + + +def select_flipped(context): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + bpy.context.scene.tool_settings.uv_select_mode = 'FACE' + bpy.ops.uv.select_all(action='SELECT') + + islands = utilities_uv.getSelectionIslands() + + + bpy.context.scene.tool_settings.uv_select_mode = 'FACE' + bpy.context.scene.tool_settings.use_uv_select_sync = False + bpy.ops.uv.select_all(action='DESELECT') + + + for island in islands: + + is_flipped = False + for face in island: + if is_flipped: + break + + # Using 'Sum of Edges' to detect counter clockwise https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order + sum = 0 + count = len(face.loops) + for i in range(count): + uv_A = face.loops[i][uv_layers].uv + uv_B = face.loops[(i+1)%count][uv_layers].uv + sum += (uv_B.x - uv_A.x) * (uv_B.y + uv_A.y) + + if sum > 0: + # Flipped + is_flipped = True + break + + # Select Island if flipped + if is_flipped: + for face in island: + for loop in face.loops: + loop[uv_layers].select = True + + + + +class Island_bounds: + faces = [] + center = Vector([0,0]) + min = Vector([0,0]) + max = Vector([0,0]) + + def __init__(self, faces): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + # Collect topology stats + self.faces = faces + + #Select Island + bpy.ops.uv.select_all(action='DESELECT') + utilities_uv.set_selected_faces(faces) + + bounds = utilities_uv.getSelectionBBox() + self.center = bounds['center'] + self.min = bounds['min'] + self.max = bounds['max'] + + + + def isEqual(A, B): + + # Bounding Box AABB intersection? + min_x = max(A.min.x, B.min.x) + min_y = max(A.min.y, B.min.y) + max_x = min(A.max.x, B.max.x) + max_y = min(A.max.y, B.max.y) + if not (max_x < min_x or max_y < min_y): + return True + + + return False + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_select_islands_identical.py b/op_select_islands_identical.py new file mode 100644 index 0000000..5912f2c --- /dev/null +++ b/op_select_islands_identical.py @@ -0,0 +1,130 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_select_islands_identical" + bl_label = "Select identical" + bl_description = "Select identical UV islands with similar topology" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + ##Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + #Not in Synced mode + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + + return True + + + def execute(self, context): + swap(self, context) + return {'FINISHED'} + + +def swap(self, context): + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + # Get selected island + islands = utilities_uv.getSelectionIslands() + + if len(islands) != 1: + self.report({'ERROR_INVALID_INPUT'}, "Please select only 1 UV Island") + return + + island_stats_source = Island_stats(islands[0]) + + bpy.context.scene.tool_settings.uv_select_mode = 'FACE' + bpy.ops.uv.select_all(action='SELECT') + + islands_all = utilities_uv.getSelectionIslands() + islands_equal = [] + for island in islands_all: + island_stats = Island_stats(island) + + if island_stats_source.isEqual(island_stats): + islands_equal.append(island_stats.faces) + + print("Islands: "+str(len(islands_equal))+"x") + + bpy.ops.uv.select_all(action='DESELECT') + for island in islands_equal: + for face in island: + for loop in face.loops: + if not loop[uv_layers].select: + loop[uv_layers].select = True + + + +class Island_stats: + countFaces = 0 + countVerts = 0 + faces = [] + area = 0 + countLinkedEdges = 0 + countLinkedFaces = 0 + + + def __init__(self, faces): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + # Collect topology stats + self.faces = faces + verts = [] + for face in faces: + self.countFaces+=1 + self.area+=face.calc_area() + + for loop in face.loops: + if loop.vert not in verts: + verts.append(loop.vert) + self.countVerts+=1 + self.countLinkedEdges+= len(loop.vert.link_edges) + self.countLinkedFaces+= len(loop.vert.link_faces) + + def isEqual(self, other): + if self.countVerts != other.countVerts: + return False + if self.countFaces != other.countFaces: + return False + + if self.countLinkedEdges != other.countLinkedEdges: + return False + if self.countLinkedFaces != other.countLinkedFaces: + return False + + # area needs to be 90%+ identical + if min(self.area, other.area)/max(self.area, other.area) < 0.7: + return False + + return True + + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_select_islands_outline.py b/op_select_islands_outline.py new file mode 100644 index 0000000..750445c --- /dev/null +++ b/op_select_islands_outline.py @@ -0,0 +1,78 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv +from . import utilities_ui + +class op(bpy.types.Operator): + bl_idname = "uv.textools_select_islands_outline" + bl_label = "Select Overlap" + bl_description = "Select island edge bounds" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + return True + + + def execute(self, context): + select_outline(context) + return {'FINISHED'} + + +def select_outline(context): + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + + + bpy.context.scene.tool_settings.use_uv_select_sync = False + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + bpy.ops.mesh.select_all(action='DESELECT') + + # Store previous edge seams + edges_seam = [edge for edge in bm.edges if edge.seam] + + + contextViewUV = utilities_ui.GetContextViewUV() + if not contextViewUV: + self.report({'ERROR_INVALID_INPUT'}, "This tool requires an available UV/Image view.") + return + + # Create seams from islands + bpy.ops.uv.seams_from_islands(contextViewUV) + edges_islands = [edge for edge in bm.edges if edge.seam] + + # Clear seams + for edge in edges_islands: + edge.seam = False + + # Select island edges + bpy.ops.mesh.select_all(action='DESELECT') + for edge in edges_islands: + edge.select = True + + # Restore seam selection + for edge in edges_seam: + edge.seam = True + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_select_islands_overlap.py b/op_select_islands_overlap.py new file mode 100644 index 0000000..a5c4263 --- /dev/null +++ b/op_select_islands_overlap.py @@ -0,0 +1,140 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv +import imp +imp.reload(utilities_uv) + +class op(bpy.types.Operator): + bl_idname = "uv.textools_select_islands_overlap" + bl_label = "Select outline" + bl_description = "Select all overlapping UV islands" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + ##Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + #Not in Synced mode + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + + return True + + + def execute(self, context): + + selectOverlap(context) + return {'FINISHED'} + + + +def selectOverlap(context): + print("Execute op_select_islands_overlap") + + # https://developer.blender.org/D2865 + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + bpy.context.scene.tool_settings.uv_select_mode = 'FACE' + bpy.ops.uv.select_all(action='SELECT') + + islands_all = utilities_uv.getSelectionIslands() + # count = len(islands_all) + + islands_bounds = [] + for island in islands_all: + islands_bounds.append( Island_bounds( island ) ) + + + groups = [] + unmatched = islands_bounds.copy() + + for islandA in islands_bounds: + if islandA in unmatched: + + group = [islandA] + for islandB in unmatched: + if islandA != islandB and islandA.isEqual(islandB): + group.append(islandB) + + for item in group: + unmatched.remove(item) + + groups.append(group) + + print("Group: {} islands, unmatched: {}x".format(len(group), len(unmatched))) + # groups.append( ) + + + bpy.ops.uv.select_all(action='DESELECT') + for group in groups: + if len(group) > 1: + for i in range(1, len(group)): + utilities_uv.set_selected_faces( group[i].faces ) + + + print("Groups: "+str(len(groups))) + + + + +class Island_bounds: + faces = [] + center = Vector([0,0]) + min = Vector([0,0]) + max = Vector([0,0]) + + def __init__(self, faces): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + # Collect topology stats + self.faces = faces + + #Select Island + bpy.ops.uv.select_all(action='DESELECT') + utilities_uv.set_selected_faces(faces) + + bounds = utilities_uv.getSelectionBBox() + self.center = bounds['center'] + self.min = bounds['min'] + self.max = bounds['max'] + + + + def isEqual(A, B): + + # Bounding Box AABB intersection? + min_x = max(A.min.x, B.min.x) + min_y = max(A.min.y, B.min.y) + max_x = min(A.max.x, B.max.x) + max_y = min(A.max.y, B.max.y) + if not (max_x < min_x or max_y < min_y): + return True + + + return False + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_smoothing_uv_islands.py b/op_smoothing_uv_islands.py new file mode 100644 index 0000000..24ed1af --- /dev/null +++ b/op_smoothing_uv_islands.py @@ -0,0 +1,67 @@ +import bpy +import bmesh +import operator +import math +from mathutils import Vector +from collections import defaultdict + + +from . import utilities_uv +from . import utilities_ui + +class op(bpy.types.Operator): + bl_idname = "uv.textools_smoothing_uv_islands" + bl_label = "Apply smooth normals and hard edges for UV Island borders." + bl_description = "Set mesh smoothing by uv islands" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + return True + + def execute(self, context): + smooth_uv_islands(self, context) + return {'FINISHED'} + + + +def smooth_uv_islands(self, context): + if bpy.context.active_object.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + # Smooth everything + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.faces_shade_smooth() + bpy.ops.mesh.mark_sharp(clear=True) + + # Select Edges + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + bpy.ops.uv.textools_select_islands_outline() + bpy.ops.mesh.mark_sharp() + bpy.ops.mesh.select_all(action='DESELECT') + + # Apply Edge split modifier + bpy.context.object.data.use_auto_smooth = True + bpy.context.object.data.auto_smooth_angle = math.pi + + # bpy.ops.object.modifier_add(type='EDGE_SPLIT') + # bpy.context.object.modifiers["EdgeSplit"].use_edge_angle = False + + bpy.ops.object.mode_set(mode='OBJECT') + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_texel_checker_map.py b/op_texel_checker_map.py new file mode 100644 index 0000000..6b75dda --- /dev/null +++ b/op_texel_checker_map.py @@ -0,0 +1,250 @@ +import bpy +import os +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_texel + + +texture_modes = ['UV_GRID','COLOR_GRID','GRAVITY','NONE'] + + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_texel_checker_map" + bl_label = "Checker Map" + bl_description = "Add a checker map to the selected model and UV view" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if len(get_valid_objects()) == 0: + return False + + return True + + def execute(self, context): + assign_checker_map( + bpy.context.scene.texToolsSettings.size[0], + bpy.context.scene.texToolsSettings.size[1] + ) + return {'FINISHED'} + + + + +def assign_checker_map(size_x, size_y): + # Force Object mode + if bpy.context.view_layer.objects.active != None and bpy.context.object.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Collect Objects + objects = get_valid_objects() + + if len(objects) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No UV mapped objects selected" ) + + #Change View mode to TEXTURED + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + for space in area.spaces: + if space.type == 'VIEW_3D': + space.shading.type = 'MATERIAL' + + + if len(objects) > 0: + + # Detect current Checker modes + mode_count = {} + for mode in texture_modes: + mode_count[mode] = 0 + + # Image sizes + image_sizes_x = [] + image_sizes_y = [] + + # Collect current modes in selected objects + for obj in objects: + image = utilities_texel.get_object_texture_image(obj) + mode = 'NONE' + if image: + if "GRAVITY" in image.name.upper(): + mode = 'GRAVITY' + + elif image.generated_type in texture_modes: + # Generated checker maps + mode = image.generated_type + + # Track image sizes + if image.size[0] not in image_sizes_x: + image_sizes_x.append(image.size[0]) + if image.size[1] not in image_sizes_y: + image_sizes_y.append(image.size[1]) + + mode_count[mode]+=1 + + + # Sort by count (returns tuple list of key,value) + mode_max_count = sorted(mode_count.items(), key=operator.itemgetter(1)) + mode_max_count.reverse() + + for key,val in mode_max_count: + print("{} = {}".format(key, val)) + + + # Determine next mode + mode = 'NONE' + if mode_max_count[0][1] == 0: + # There are no maps + mode = texture_modes[0] + + elif mode_max_count[0][0] in texture_modes: + if mode_max_count[-1][1] > 0: + # There is more than 0 of another mode, complete existing mode first + mode = mode_max_count[0][0] + + else: + # Switch to next checker mode + index = texture_modes.index(mode_max_count[0][0]) + + if texture_modes[ index ] != 'NONE' and len(image_sizes_x) > 1 or len(image_sizes_y) > 1: + # There are multiple resolutions on selected objects + mode = texture_modes[ index ] + elif texture_modes[ index ] != 'NONE' and (len(image_sizes_x) > 0 and image_sizes_x[0] != size_x) and (len(image_sizes_y) > 0 and image_sizes_y[0] != size_y): + # The selected objects have a different resolution + mode = texture_modes[ index ] + else: + # Next mode + mode = texture_modes[ (index+1)%len(texture_modes) ] + + + print("Mode: "+mode) + + if mode == 'NONE': + for obj in objects: + remove_material(obj) + + elif mode == 'GRAVITY': + image = load_image("checker_map_gravity") + for obj in objects: + apply_image(obj, image) + + else: + name = utilities_texel.get_checker_name(mode, size_x, size_y) + image = get_image(name, mode, size_x, size_y) + for obj in objects: + apply_image(obj, image) + + # Restore object selection + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + for obj in objects: + obj.select_set( state = True, view_layer = None) + + # Clean up images and materials + utilities_texel.checker_images_cleanup() + + # Force redraw of viewport to update texture + # bpy.context.scene.update() + bpy.context.view_layer.update() + + + + +def load_image(name): + pathTexture = icons_dir = os.path.join(os.path.dirname(__file__), "resources/{}.png".format(name)) + image = bpy.ops.image.open(filepath=pathTexture, relative_path=False) + if "{}.png".format(name) in bpy.data.images: + bpy.data.images["{}.png".format(name)].name = name #remove extension in name + return bpy.data.images[name]; + + + +def get_valid_objects(): + # Collect Objects + objects = [] + for obj in bpy.context.selected_objects: + if obj.type == 'MESH' and obj.data.uv_layers: + objects.append(obj) + + return objects + + + + + + + +def remove_material(obj): + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + count = len(obj.material_slots) + for i in range(count): + bpy.ops.object.material_slot_remove() + + + +def apply_image(obj, image): + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + + # Assign Cycles material with image + + # Get Material + material = None + if image.name in bpy.data.materials: + material = bpy.data.materials[image.name] + else: + material = bpy.data.materials.new(image.name) + material.use_nodes = True + + # Assign material + if len(obj.data.materials) > 0: + obj.data.materials[0] = material + else: + obj.data.materials.append(material) + + # Setup Node + tree = material.node_tree + node = None + if "checker" in tree.nodes: + node = tree.nodes["checker"] + else: + node = tree.nodes.new("ShaderNodeTexImage") + node.name = "checker" + node.select = True + tree.nodes.active = node + node.image = image + + # LINKANDO: + tree = obj.data.materials[0].node_tree + links = tree.links + nodo1 = tree.nodes['checker'] + nodo2 = tree.nodes['Principled BSDF'] + links.new(nodo1.outputs['Color'], nodo2.inputs['Base Color']) + + +def get_image(name, mode, size_x, size_y): + # Image already exists? + if name in bpy.data.images: + # Update texture UV checker mode + bpy.data.images[name].generated_type = mode + return bpy.data.images[name]; + + # Create new image instead + image = bpy.data.images.new(name, width=size_x, height=size_y) + image.generated_type = mode #UV_GRID or COLOR_GRID + image.generated_width = int(size_x) + image.generated_height = int(size_y) + + return image + +bpy.utils.register_class(op) diff --git a/op_texel_density_get.py b/op_texel_density_get.py new file mode 100644 index 0000000..4504638 --- /dev/null +++ b/op_texel_density_get.py @@ -0,0 +1,133 @@ +import bpy +import bmesh +import operator +import math + +from . import utilities_texel + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_texel_density_get" + bl_label = "Get Texel size" + bl_description = "Get Pixel per unit ratio or Texel density" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + if not bpy.context.active_object: + return False + + if len(bpy.context.selected_objects) == 0: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + if not bpy.context.object.data.uv_layers: + return False + + # if bpy.context.object.mode == 'EDIT': + # # In edit mode requires face select mode + # if bpy.context.scene.tool_settings.mesh_select_mode[2] == False: + # return False + + return True + + def execute(self, context): + get_texel_density( + self, + context + ) + return {'FINISHED'} + + + +def get_texel_density(self, context): + print("Get texel density") + + edit_mode = bpy.context.object.mode == 'EDIT' + object_faces = utilities_texel.get_selected_object_faces() + + # Warning: No valid input objects + if len(object_faces) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No UV maps or meshes selected" ) + return + + print("obj faces groups {}".format(len(object_faces))) + + # Collect Images / textures + object_images = {} + for obj in object_faces: + image = utilities_texel.get_object_texture_image(obj) + if image: + object_images[obj] = image + + # Warning: No valid images + if len(object_images) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No Texture found. Assign Checker map or texture first." ) + return + + sum_area_vt = 0 + sum_area_uv = 0 + + # Get area for each triangle in view and UV + for obj in object_faces: + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set( state = True, view_layer = None) + + # Find image of object + image = object_images[obj] + if image: + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + uv_layers = bm.loops.layers.uv.verify() + bm.faces.ensure_lookup_table() + + for index in object_faces[obj]: + face = bm.faces[index] + + # Triangle Verts + triangle_uv = [loop[uv_layers].uv for loop in face.loops ] + triangle_vt = [vert.co for vert in face.verts] + + #Triangle Areas + face_area_vt = utilities_texel.get_area_triangle( + triangle_vt[0], + triangle_vt[1], + triangle_vt[2] + ) + face_area_uv = utilities_texel.get_area_triangle_uv( + triangle_uv[0], + triangle_uv[1], + triangle_uv[2], + image.size[0], + image.size[1] + ) + sum_area_vt+= math.sqrt( face_area_vt ) + sum_area_uv+= math.sqrt( face_area_uv ) * min(image.size[0], image.size[1]) + + # Restore selection + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + for obj in object_faces: + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = list(object_faces.keys())[0] + if edit_mode: + bpy.ops.object.mode_set(mode='EDIT') + + # print("Sum verts area {}".format(sum_area_vt)) + # print("Sum texture area {}".format(sum_area_uv)) + + if sum_area_uv == 0 or sum_area_vt == 0: + bpy.context.scene.texToolsSettings.texel_density = 0 + else: + bpy.context.scene.texToolsSettings.texel_density = sum_area_uv / sum_area_vt + +bpy.utils.register_class(op) + \ No newline at end of file diff --git a/op_texel_density_set.py b/op_texel_density_set.py new file mode 100644 index 0000000..8f54662 --- /dev/null +++ b/op_texel_density_set.py @@ -0,0 +1,189 @@ +import bpy +import bmesh +import operator +import math +from mathutils import Vector +from collections import defaultdict + + +from . import utilities_texel +from . import utilities_uv + +class op(bpy.types.Operator): + bl_idname = "uv.textools_texel_density_set" + bl_label = "Set Texel size" + bl_description = "Apply texel density by scaling the UV's to match the ratio" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + + if not bpy.context.active_object: + return False + + if len(bpy.context.selected_objects) == 0: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + # if bpy.context.object.mode == 'EDIT': + # # In edit mode requires face select mode + # if bpy.context.scene.tool_settings.mesh_select_mode[2] == False: + # return False + + return True + + + def execute(self, context): + set_texel_density( + self, + context, + bpy.context.scene.texToolsSettings.texel_mode_scale, + bpy.context.scene.texToolsSettings.texel_density + ) + return {'FINISHED'} + + + +def set_texel_density(self, context, mode, density): + print("Set texel density!") + + is_edit = bpy.context.object.mode == 'EDIT' + is_sync = bpy.context.scene.tool_settings.use_uv_select_sync + object_faces = utilities_texel.get_selected_object_faces() + + + # Warning: No valid input objects + if len(object_faces) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No valid meshes or UV maps" ) + return + + # Collect Images / textures + object_images = {} + for obj in object_faces: + image = utilities_texel.get_object_texture_image(obj) + if image: + object_images[obj] = image + + # Warning: No valid images + if len(object_images) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No Texture found. Assign Checker map or texture." ) + return + + + for obj in object_faces: + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set( state = True, view_layer = None) + + # Find image of object + image = object_images[obj] + if image: + bpy.ops.object.mode_set(mode='EDIT') + bpy.context.scene.tool_settings.use_uv_select_sync = False + + # Store selection + utilities_uv.selection_store() + + bm = bmesh.from_edit_mesh(obj.data) + uv_layers = bm.loops.layers.uv.verify() + + # Collect groups of faces to scale together + group_faces = [] + if is_edit: + # Collect selected faces as islands + bm.faces.ensure_lookup_table() + bpy.ops.uv.select_all(action='SELECT') + group_faces = utilities_uv.getSelectionIslands() + + elif mode == 'ALL': + # Scale all UV's together + group_faces = [bm.faces] + + elif mode == 'ISLAND': + # Scale each UV idland centered + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.select_all(action='SELECT') + group_faces = utilities_uv.getSelectionIslands() + + print("group_faces {}x".format(len(group_faces))) + + + for group in group_faces: + # Get triangle areas + sum_area_vt = 0 + sum_area_uv = 0 + for face in group: + # Triangle Verts + triangle_uv = [loop[uv_layers].uv for loop in face.loops ] + triangle_vt = [obj.matrix_world @ vert.co for vert in face.verts] + + #Triangle Areas + face_area_vt = utilities_texel.get_area_triangle( + triangle_vt[0], + triangle_vt[1], + triangle_vt[2] + ) + face_area_uv = utilities_texel.get_area_triangle_uv( + triangle_uv[0], + triangle_uv[1], + triangle_uv[2], + image.size[0], + image.size[1] + ) + + sum_area_vt+= math.sqrt( face_area_vt ) + sum_area_uv+= math.sqrt( face_area_uv ) * min(image.size[0], image.size[1]) + + # Apply scale to group + print("scale: {:.2f} {:.2f} {:.2f} ".format(density, sum_area_uv, sum_area_vt)) + scale = 0 + if density > 0 and sum_area_uv > 0 and sum_area_vt > 0: + scale = density / (sum_area_uv / sum_area_vt) + + # Set Scale Origin to Island or top left + if mode == 'ISLAND': + bpy.context.tool_settings.transform_pivot_point = 'MEDIAN_POINT' + elif mode == 'ALL': + bpy.context.tool_settings.transform_pivot_point = 'CURSOR' + bpy.ops.uv.cursor_set(location=(0, 1)) + + # Select Face loops and scale + bpy.ops.uv.select_all(action='DESELECT') + bpy.context.scene.tool_settings.uv_select_mode = 'VERTEX' + for face in group: + for loop in face.loops: + loop[uv_layers].select = True + + print("Scale: {} {}x".format(scale, len(group))) + bpy.ops.transform.resize(value=(scale, scale, 1), use_proportional_edit=False) + + # Restore selection + utilities_uv.selection_restore() + + # Restore selection + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + for obj in object_faces: + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = list(object_faces.keys())[0] + + # Restore edit mode + if is_edit: + bpy.ops.object.mode_set(mode='EDIT') + + # Restore sync mode + if is_sync: + bpy.context.scene.tool_settings.use_uv_select_sync = True + +bpy.utils.register_class(op) diff --git a/op_texture_open.py b/op_texture_open.py new file mode 100644 index 0000000..84d448b --- /dev/null +++ b/op_texture_open.py @@ -0,0 +1,50 @@ +import bpy +import bmesh +import operator +import math +import os, sys, subprocess + +from . import settings +from . import utilities_bake + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_texture_open" + bl_label = "Open Texture" + bl_description = "Open the texture on the system" + + name : bpy.props.StringProperty( + name="image name", + default = "" + ) + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + open_texture(self, context) + return {'FINISHED'} + + + +def open_texture(self, context): + + print("Info") + if self.name in bpy.data.images: + image = bpy.data.images[self.name] + + if image.filepath != "": + path = bpy.path.abspath(image.filepath) + # https://meshlogic.github.io/posts/blender/addons/extra-image-list/ + # https://docs.blender.org/api/blender_python_api_2_78_release/bpy.ops.image.html + print("Open: {}".format(path)) + + if sys.platform == "win32": + os.startfile(path) + else: + opener ="open" if sys.platform == "darwin" else "xdg-open" + subprocess.call([opener, path]) + + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_texture_preview.py b/op_texture_preview.py new file mode 100644 index 0000000..78bab23 --- /dev/null +++ b/op_texture_preview.py @@ -0,0 +1,101 @@ +import bpy +import bmesh +import operator +import math +from mathutils import Vector +from collections import defaultdict + +from . import settings +from . import utilities_color +from . import utilities_bake + +material_prefix = "TT_atlas_" +gamma = 2.2 + +class op(bpy.types.Operator): + bl_idname = "uv.textools_texture_preview" + bl_label = "Preview Texture" + bl_description = "Preview the current UV image view background image on the selected object." + bl_options = {'REGISTER', 'UNDO'} + + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if len(settings.sets) == 0: + return False + + # Only when we have a background image + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + return area.spaces[0].image + + return False + + def execute(self, context): + print("PREVIEW TEXTURE????") + preview_texture(self, context) + return {'FINISHED'} + + + +def preview_texture(self, context): + + # Collect all low objects from bake sets + objects = [obj for s in settings.sets for obj in s.objects_low if obj.data.uv_layers] + + # Get view 3D area + view_area = None + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + view_area = area + + # Exit existing local view + # if view_area and view_area.spaces[0].local_view: + # bpy.ops.view3d.localview({'area': view_area}) + # return + + + # Get background image + image = None + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + image = area.spaces[0].image + break + + if image: + for obj in objects: + print("Map {}".format(obj.name)) + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + + for i in range(len(obj.material_slots)): + bpy.ops.object.material_slot_remove() + + #Create material with image + bpy.ops.object.material_slot_add() + obj.material_slots[0].material = utilities_bake.get_image_material(image) + obj.display_type = 'TEXTURED' + + + # Re-Select objects + bpy.ops.object.select_all(action='DESELECT') + for obj in objects: + obj.select_set( state = True, view_layer = None) + + if view_area: + #Change View mode to TEXTURED + for space in view_area.spaces: + if space.type == 'VIEW_3D': + space.shading.type = 'MATERIAL' + + # Enter local view + # bpy.ops.view3d.localview({'area': view_area}) + # bpy.ops.ui.textools_popup('INVOKE_DEFAULT', message="Object is in isolated view") + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_texture_reload_all.py b/op_texture_reload_all.py new file mode 100644 index 0000000..4e342c7 --- /dev/null +++ b/op_texture_reload_all.py @@ -0,0 +1,64 @@ +import bpy +import os +import bmesh +from mathutils import Vector +from collections import defaultdict +from math import pi + +class op(bpy.types.Operator): + bl_idname = "uv.textools_texture_reload_all" + bl_label = "Reload Textures and remove unused Textures" + bl_description = "Reload all textures" + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + main(context) + return {'FINISHED'} + + +def main(context): + count_clear_mat = 0 + count_clear_img = 0 + count_reload = 0 + + for material in bpy.data.materials: + if not material.users: + count_clear_mat+=1 + material.user_clear() + bpy.data.materials.remove(material) + + # Clean up unused images + for image in bpy.data.images: + if not image.users: + count_clear_img+=1 + image.user_clear() + bpy.data.images.remove(image) + + #Reload all File images + for img in bpy.data.images : + if img.source == 'FILE' : + count_reload+=1 + img.reload() + + # Refresh vieport texture + for window in bpy.context.window_manager.windows: + screen = window.screen + for area in screen.areas: + area.tag_redraw() + + # Show popup on cleared & reloaded items + message = "" + if count_reload > 0: + message+="{}x reloaded. ".format(count_reload) + if count_clear_mat > 0: + message+="{}x mat cleared. ".format(count_clear_mat) + if count_clear_img > 0: + message+="{}x img cleared.".format(count_clear_img) + + if len(message) > 0: + bpy.ops.ui.textools_popup('INVOKE_DEFAULT', message=message) + +bpy.utils.register_class(op) diff --git a/op_texture_remove.py b/op_texture_remove.py new file mode 100644 index 0000000..d4ff43f --- /dev/null +++ b/op_texture_remove.py @@ -0,0 +1,43 @@ +import bpy +import bmesh +import operator +import math +import os + +from bpy.props import * +from . import settings +from . import utilities_bake + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_texture_remove" + bl_label = "Remove Texture" + bl_description = "Remove the texture" + bl_options = {'REGISTER', 'UNDO'} + + name : bpy.props.StringProperty( + name="image name", + default = "" + ) + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + remove_texture(self.name) + return {'FINISHED'} + + + + + +def remove_texture(name): + print("Save image.. "+name) + + + if name in bpy.data.images: + bpy.data.images.remove( bpy.data.images[name] ) + + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_texture_save.py b/op_texture_save.py new file mode 100644 index 0000000..7d87628 --- /dev/null +++ b/op_texture_save.py @@ -0,0 +1,109 @@ +import bpy +import bmesh +import operator +import math +import os + +from bpy.props import * +from . import settings +from . import utilities_bake + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_texture_save" + bl_label = "Save Texture" + bl_description = "Save the texture" + + name : bpy.props.StringProperty( + name="image name", + default = "" + ) + + # Properties used by the file browser + # filepath = bpy.props.StringProperty(subtype="FILE_PATH") + # http://nullege.com/codes/show/src%40b%40l%40blenderpython-HEAD%40scripts%40addons_extern%40io_scene_valvesource%40import_smd.py/90/bpy.context.window_manager.fileselect_add/python + filepath : bpy.props.StringProperty(name="myName.png", description="Texture filepath", maxlen=1024, default="bla bla.png") + filter_folder : BoolProperty(name="Filter folders", description="", default=True, options={'HIDDEN'}) + filter_glob : StringProperty(default="*.png;*.tga;*.jpg;*.tif;*.exr", options={'HIDDEN'}) + + def invoke(self, context, event): + # if self.filepath == "": + # self.filepath = bpy.context.scene.FBXBundleSettings.path + # blend_filepath = context.blend_data.filepath + # https://blender.stackexchange.com/questions/6159/changing-default-text-value-in-file-dialogue + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + + def draw(self, context): + layout = self.layout + + layout.label(text="Choose your Unity Asset directory") + + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + save_texture(self.filepath) + return {'FINISHED'} + + + +def save_texture(path): + print("Save image.. "+path) + + + + + +# class op(bpy.types.Operator): +# bl_idname = "uv.textools_texture_save" +# bl_label = "Save Texture" +# bl_description = "Save the texture" + +# name = bpy.props.StringProperty( +# name="image name", +# default = "" +# ) + +# @classmethod +# def poll(cls, context): +# return True + +# def execute(self, context): +# save_texture(self, context) +# return {'FINISHED'} + + + + + + + +''' +class op_ui_image_save(bpy.types.Operator): + bl_idname = "uv.textools_ui_image_save" + bl_label = "Save image" + bl_description = "Save this image" + + image_name = bpy.props.StringProperty( + name="image name", + default = "" + ) + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + # bpy.context.scene.tool_settings.use_uv_select_sync = False + # bpy.ops.mesh.select_all(action='SELECT') + + print("Saving image {}".format(self.image_name)) + # bpy.ops.image.save_as() + return {'FINISHED'} + +''' +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_texture_select.py b/op_texture_select.py new file mode 100644 index 0000000..c020a57 --- /dev/null +++ b/op_texture_select.py @@ -0,0 +1,84 @@ +import bpy +import bmesh +import operator +import math + +from . import settings +from . import utilities_bake +from . import op_bake + +class op(bpy.types.Operator): + bl_idname = "uv.textools_texture_select" + bl_label = "Select Texture" + bl_description = "Select the texture and bake mode" + bl_options = {'REGISTER', 'UNDO'} + + name : bpy.props.StringProperty( + name="image name", + default = "" + ) + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + select_texture(self, context) + return {'FINISHED'} + + + +def select_texture(self, context): + print("Select "+self.name) + + + # Set bake mode + for mode in op_bake.modes: + if mode in self.name: + print("Found mode: "+mode) + + prop = bpy.context.scene.bl_rna.properties["TT_bake_mode"] + enum_values = [e.identifier for e in prop.enum_items] + + # find matching enum + for key in enum_values: + print("TT_bake "+key) + if mode in key: + print("set m: "+key) + bpy.context.scene.TT_bake_mode = key + break; + + break + + # Set background image + if self.name in bpy.data.images: + image = bpy.data.images[self.name] + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + area.spaces[0].image = image + + +''' +class op_ui_image_select(bpy.types.Operator): + bl_idname = "uv.textools_ui_image_select" + bl_label = "Select image" + bl_description = "Select this image" + + image_name = bpy.props.StringProperty( + name="image name", + default = "" + ) + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + # bpy.context.scene.tool_settings.use_uv_select_sync = False + # bpy.ops.mesh.select_all(action='SELECT') + + print("Select image {}".format(self.image_name)) + # bpy.ops.image.save_as() + return {'FINISHED'} +''' +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_unwrap_edge_peel.py b/op_unwrap_edge_peel.py new file mode 100644 index 0000000..6aaf0f6 --- /dev/null +++ b/op_unwrap_edge_peel.py @@ -0,0 +1,95 @@ +import bpy +import os +import bmesh +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv +from . import utilities_ui + +class op(bpy.types.Operator): + bl_idname = "uv.textools_unwrap_edge_peel" + bl_label = "Peel Edge" + bl_description = "Unwrap pipe along selected edges" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + # Need view Face mode + if tuple(bpy.context.scene.tool_settings.mesh_select_mode)[1] == False: + return False + + return True + + def execute(self, context): + unwrap_edges_pipe(self, context) + return {'FINISHED'} + + +def unwrap_edges_pipe(self, context): + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + + contextViewUV = utilities_ui.GetContextViewUV() + if not contextViewUV: + self.report({'ERROR_INVALID_INPUT'}, "This tool requires an available UV/Image view.") + return + + # selected_initial = [edge for edge in bm.edges if edge.select] + selected_edges = [] + selected_faces = [] + + # Extend loop selection + bpy.ops.mesh.loop_multi_select(ring=False) + selected_edges = [edge for edge in bm.edges if edge.select] + + if len(selected_edges) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No edges selected in the view" ) + return + + # Convert linked selection to single UV island + bpy.ops.mesh.select_linked(delimit=set()) + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + bpy.ops.uv.textools_unwrap_faces_iron() + selected_faces = [face for face in bm.faces if face.select] + + if len(selected_faces) == 0: + self.report({'ERROR_INVALID_INPUT'}, "No faces available" ) + return + + # Mark previous selected edges as Seam + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + for edge in selected_edges: + edge.select = True + bpy.ops.mesh.mark_seam(clear=False) + + # Follow active quad unwrap for faces + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + for face in selected_faces: + face.select = True + bm.faces.active = selected_faces[0] + + bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.0226216) + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.uv.textools_rectify(contextViewUV) + + # TODO: Restore initial selection + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_unwrap_faces_iron.py b/op_unwrap_faces_iron.py new file mode 100644 index 0000000..b324fea --- /dev/null +++ b/op_unwrap_faces_iron.py @@ -0,0 +1,76 @@ +import bpy +import os +import bmesh +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv + +class op(bpy.types.Operator): + """UV Operator description""" + bl_idname = "uv.textools_unwrap_faces_iron" + bl_label = "Iron" + bl_description = "Unwrap selected faces into a single UV island" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + # Need view Face mode + if tuple(bpy.context.scene.tool_settings.mesh_select_mode)[2] == False: + return False + #Only in UV editor mode + # if bpy.context.area.type != 'IMAGE_EDITOR': + # return False + + #Requires UV map + # if not bpy.context.object.data.uv_layers: + # return False + + # if bpy.context.scene.tool_settings.uv_select_mode != 'FACE': + # return False + return True + + def execute(self, context): + main(context) + return {'FINISHED'} + + +def main(context): + print("operatyor_faces_iron()") + + #Store selection + utilities_uv.selection_store() + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + bpy.context.scene.tool_settings.uv_select_mode = 'FACE' + bpy.ops.mesh.mark_seam(clear=True) + + + selected_faces = [f for f in bm.faces if f.select] + + # Hard edges to seams + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') + bpy.ops.mesh.region_to_loop() + bpy.ops.mesh.mark_seam(clear=False) + + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + for face in selected_faces: + face.select = True + + bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.0226216) + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_uv_channel_add.py b/op_uv_channel_add.py new file mode 100644 index 0000000..dad9635 --- /dev/null +++ b/op_uv_channel_add.py @@ -0,0 +1,67 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_ui + +class op(bpy.types.Operator): + bl_idname = "uv.textools_uv_channel_add" + bl_label = "Add UV Channel" + bl_description = "Add a new UV channel with smart UV projected UV's and padding." + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if bpy.context.active_object == None: + return False + if bpy.context.active_object.type != 'MESH': + return False + return True + + + @classmethod + def poll(cls, context): + if bpy.context.active_object == None: + return False + if bpy.context.active_object.type != 'MESH': + return False + if len(bpy.context.selected_objects) != 1: + return False + + return True + + def execute(self, context): + print("Add UV") + + if len( bpy.context.object.data.uv_layers ) == 0: + # Create first UV channel + if bpy.context.active_object.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + + # Smart project UV's + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.smart_project( + angle_limit=65, + island_margin=0.5, + user_area_weight=0, + use_aspect=True, + stretch_to_bounds=True + ) + + # Re-Apply padding as normalized values + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.uv.pack_islands(margin=utilities_ui.get_padding()) + else: + # Add new UV channel based on last + bpy.ops.mesh.uv_texture_add() + + # Get current index + index = len(bpy.context.object.data.uv_layers)-1 + bpy.context.object.data.uv_layers.active_index = index + bpy.context.scene.texToolsSettings.uv_channel = str(index) + return {'FINISHED'} + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_uv_channel_swap.py b/op_uv_channel_swap.py new file mode 100644 index 0000000..68158e5 --- /dev/null +++ b/op_uv_channel_swap.py @@ -0,0 +1,70 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_uv_channel_swap" + bl_label = "Move UV Channel" + bl_description = "Move UV channel up or down" + bl_options = {'REGISTER', 'UNDO'} + + is_down : bpy.props.BoolProperty(default=False) + + @classmethod + def poll(cls, context): + if bpy.context.active_object == None: + return False + if bpy.context.active_object.type != 'MESH': + return False + if len(bpy.context.object.data.uv_layers) <= 1: + return False + return True + + + def execute(self, context): + uv_layers = bpy.context.object.data.uv_layers + + if uv_layers.active_index == 0 and not self.is_down: + return {'FINISHED'} + elif uv_layers.active_index == len(uv_layers)-1 and self.is_down: + return {'FINISHED'} + + def get_index(name): + return ([i for i in range(len(uv_layers)) if uv_layers[i].name == name])[0] + + def move_bottom(name): + # Set index + uv_layers.active_index = get_index(name) + # Copy (to bottom) + bpy.ops.mesh.uv_texture_add() + # Delete previous + uv_layers.active_index = get_index(name) + bpy.ops.mesh.uv_texture_remove() + # Rename new + uv_layers.active_index = len(uv_layers)-1 + uv_layers.active.name = name + + count = len(uv_layers) + + index_A = uv_layers.active_index + index_B = index_A + (1 if self.is_down else -1) + + if not self.is_down: + # Move up + for n in [uv_layers[i].name for i in range(index_B, count) if i != index_A]: + move_bottom(n) + bpy.context.scene.texToolsSettings.uv_channel = str(index_B) + + elif self.is_down: + # Move down + for n in [uv_layers[i].name for i in range(index_A, count) if i != index_B]: + move_bottom(n) + bpy.context.scene.texToolsSettings.uv_channel = str(index_B) + + return {'FINISHED'} + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_uv_crop.py b/op_uv_crop.py new file mode 100644 index 0000000..842cd50 --- /dev/null +++ b/op_uv_crop.py @@ -0,0 +1,66 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv +from . import utilities_ui + +class op(bpy.types.Operator): + bl_idname = "uv.textools_uv_crop" + bl_label = "Crop" + bl_description = "Crop UV area to selected UV faces" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + return True + + def execute(self, context): + crop(self, context) + return {'FINISHED'} + + + +def crop(self, context): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + padding = utilities_ui.get_padding() + + + # Scale to fit bounds + bbox = utilities_uv.getSelectionBBox() + scale_u = (1.0-padding) / bbox['width'] + scale_v = (1.0-padding) / bbox['height'] + scale = min(scale_u, scale_v) + + bpy.ops.transform.resize(value=(scale, scale, scale), constraint_axis=(False, False, False), mirror=False, use_proportional_edit=False) + + # Reposition + bbox = utilities_uv.getSelectionBBox() + + delta_position = Vector((padding/2,1-padding/2)) - Vector((bbox['min'].x, bbox['min'].y + bbox['height'])) + bpy.ops.transform.translate(value=(delta_position.x, delta_position.y, 0)) + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_uv_fill.py b/op_uv_fill.py new file mode 100644 index 0000000..683bda4 --- /dev/null +++ b/op_uv_fill.py @@ -0,0 +1,117 @@ +import bpy +import bmesh +import operator +import math + +from mathutils import Vector +from collections import defaultdict + + +from . import utilities_uv +from . import utilities_ui + +class op(bpy.types.Operator): + bl_idname = "uv.textools_uv_fill" + bl_label = "Fill" + bl_description = "Fill UV selection to UV canvas" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + return True + + def execute(self, context): + fill(self, context) + return {'FINISHED'} + + + +def fill(self, context): + + + #Store selection + utilities_uv.selection_store() + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + + # 1.) Rotate minimal bounds (less than 45 degrees rotation) + steps = 8 + angle = 45; # Starting Angle, half each step + bboxPrevious = utilities_uv.getSelectionBBox() + + for i in range(0, steps): + # Rotate right + bpy.ops.transform.rotate(value=(angle * math.pi / 180), orient_axis='Z') + bbox = utilities_uv.getSelectionBBox() + + + print("Rotate {}, diff le: {}".format(angle, bbox['height'] - bboxPrevious['height'])) + + + if i == 0: + # Check if already squared + sizeA = bboxPrevious['width'] * bboxPrevious['height'] + sizeB = bbox['width'] * bbox['height'] + if abs(bbox['width'] - bbox['height']) <= 0.0001 and sizeA < sizeB: + bpy.ops.transform.rotate(value=(-angle * math.pi / 180), orient_axis='Z') + break; + + if bbox['minLength'] < bboxPrevious['minLength']: + bboxPrevious = bbox; # Success + else: + # Rotate Left + bpy.ops.transform.rotate(value=(-angle*2 * math.pi / 180), orient_axis='Z') + bbox = utilities_uv.getSelectionBBox() + if bbox['minLength'] < bboxPrevious['minLength']: + bboxPrevious = bbox; # Success + else: + # Restore angle of this iteration + bpy.ops.transform.rotate(value=(angle * math.pi / 180), orient_axis='Z') + + angle = angle / 2 + + if bboxPrevious['width'] < bboxPrevious['height']: + bpy.ops.transform.rotate(value=(90 * math.pi / 180), orient_axis='Z') + + # 2.) Match width and height to UV bounds + bbox = utilities_uv.getSelectionBBox() + + scale_x = 1.0 / bbox['width'] + scale_y = 1.0 / bbox['height'] + + print("Scale {} | {}".format(scale_x, scale_y)) + + bpy.context.tool_settings.transform_pivot_point = 'BOUNDING_BOX_CENTER' + bpy.ops.transform.resize(value=(scale_x, scale_y, 1), constraint_axis=(False, False, False), orient_type='GLOBAL', use_proportional_edit=False) + + + bbox = utilities_uv.getSelectionBBox() + offset_x = -bbox['min'].x + offset_y = -bbox['min'].y + + bpy.ops.transform.translate(value=(offset_x, offset_y, 0), constraint_axis=(False, False, False), orient_type='GLOBAL', use_proportional_edit=False) + + #Restore selection + utilities_uv.selection_restore() + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_uv_resize.py b/op_uv_resize.py new file mode 100644 index 0000000..39b4138 --- /dev/null +++ b/op_uv_resize.py @@ -0,0 +1,264 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_uv +from . import utilities_ui +from . import utilities_texel + +name_texture = "TT_resize_area" + + +utilities_ui.icon_register("op_extend_canvas_TL_active.png") +utilities_ui.icon_register("op_extend_canvas_TR_active.png") +utilities_ui.icon_register("op_extend_canvas_BL_active.png") +utilities_ui.icon_register("op_extend_canvas_BR_active.png") + + + +def on_dropdown_size_x(self, context): + self.size_x = int(self.dropdown_size_x) + # context.area.tag_redraw() + +def on_dropdown_size_y(self, context): + self.size_y = int(self.dropdown_size_y) + # context.area.tag_redraw() + + +class op(bpy.types.Operator): + bl_idname = "uv.textools_uv_resize" + bl_label = "Resize Area" + bl_description = "Resize or extend the UV area" + bl_options = {'REGISTER', 'UNDO'} + + size_x : bpy.props.IntProperty( + name = "Width", + description="padding size in pixels", + default = 1024, + min = 1, + max = 8192 + ) + size_y : bpy.props.IntProperty( + name = "Height", + description="padding size in pixels", + default = 1024, + min = 1, + max = 8192 + ) + dropdown_size_x : bpy.props.EnumProperty( + items = utilities_ui.size_textures, + name = "", + update = on_dropdown_size_x, + default = '1024' + ) + dropdown_size_y : bpy.props.EnumProperty( + items = utilities_ui.size_textures, + name = "", + update = on_dropdown_size_y, + default = '1024' + ) + + direction : bpy.props.EnumProperty(name='direction', items=( + ('TL',' ','Top Left', utilities_ui.icon_get("op_extend_canvas_TL_active"),0), + ('BL',' ','Bottom Left', utilities_ui.icon_get("op_extend_canvas_BL_active"),2), + ('TR',' ','Top Right', utilities_ui.icon_get("op_extend_canvas_TR_active"),1), + ('BR',' ','Bottom Right', utilities_ui.icon_get("op_extend_canvas_BR_active"),3) + )) + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + #Only in Edit mode + if bpy.context.active_object.mode != 'EDIT': + return False + + #Only in UV editor mode + if bpy.context.area.type != 'IMAGE_EDITOR': + return False + + #Requires UV map + if not bpy.context.object.data.uv_layers: + return False + + return True + + + def invoke(self, context, event): + print("Invoke resize area") + self.size_x = bpy.context.scene.texToolsSettings.size[0] + self.size_y = bpy.context.scene.texToolsSettings.size[1] + + for item in utilities_ui.size_textures: + if int(item[0]) == self.size_x: + self.dropdown_size_x = item[0] + break + for item in utilities_ui.size_textures: + if int(item[0]) == self.size_y: + self.dropdown_size_y = item[0] + break + + + return context.window_manager.invoke_props_dialog(self, width = 140) + + def check(self, context): + return True + + def draw(self, context): + # https://b3d.interplanety.org/en/creating-pop-up-panels-with-user-ui-in-blender-add-on/ + layout = self.layout + + + layout.separator() + + # New Size + row = layout.row() + split = row.split(factor=0.6) + c = split.column(align=True) + c.prop(self, "size_x", text="X",expand=True) + c.prop(self, "size_y", text="Y",expand=True) + + c = split.column(align=True) + c.prop(self, "dropdown_size_x", text="") + c.prop(self, "dropdown_size_y", text="") + + # Direction + col = layout.column(align=True) + col.label(text="Direction") + row = col.row(align=True) + row.prop(self,'direction', expand=True) + + # Summary + size_A = "{} x {}".format(bpy.context.scene.texToolsSettings.size[0], bpy.context.scene.texToolsSettings.size[1]) + if bpy.context.scene.texToolsSettings.size[0] == bpy.context.scene.texToolsSettings.size[1]: + size_A = "{}²".format(bpy.context.scene.texToolsSettings.size[0]) + size_B = "{} x {}".format(self.size_x, self.size_y) + if self.size_x == self.size_y: + size_B = "{}²".format(self.size_x) + + layout.label(text="{} to {}".format( + size_A, size_B + )) + + + layout.separator() + + + def execute(self, context): + + #Store selection + utilities_uv.selection_store() + + # Get start and end size + size_A = Vector([ + bpy.context.scene.texToolsSettings.size[0], + bpy.context.scene.texToolsSettings.size[1] + ]) + size_B = Vector([ + self.size_x, + self.size_y + ]) + + resize_uv( + self, + context, + self.direction, + size_A, + size_B + ) + resize_image( + context, + self.direction, + size_A, + size_B + ) + + bpy.context.scene.texToolsSettings.size[0] = self.size_x + bpy.context.scene.texToolsSettings.size[1] = self.size_y + + #Restore selection + utilities_uv.selection_restore() + + return {'FINISHED'} + + + +def resize_uv(self, context, mode, size_A, size_B): + + # Set pivot + bpy.context.tool_settings.transform_pivot_point = 'CURSOR' + if mode == 'TL': + bpy.ops.uv.cursor_set(location=Vector([0,1])) + elif mode == 'TR': + bpy.ops.uv.cursor_set(location=Vector([1,1])) + elif mode == 'BL': + bpy.ops.uv.cursor_set(location=Vector([0,0])) + elif mode == 'BR': + bpy.ops.uv.cursor_set(location=Vector([1,0])) + + # Select all UV faces + bpy.ops.uv.select_all(action='SELECT') + + # Resize + scale_x = size_A.x / size_B.x + scale_y = size_A.y / size_B.y + bpy.ops.transform.resize(value=(scale_x, scale_y, 1.0), use_proportional_edit=False) + + + +def resize_image(context, mode, size_A, size_B): + print("resize image {}".format( context.area.spaces )) + + # Notes: https://blender.stackexchange.com/questions/31514/active-image-of-uv-image-editor + # https://docs.blender.org/api/blender_python_api_2_70_4/bpy.types.SpaceImageEditor.html + + + if context.area.spaces.active != None: + if context.area.spaces.active.image != None: + image = context.area.spaces.active.image + image_obj = utilities_texel.get_object_texture_image(bpy.context.active_object) + if name_texture in image.name or image == image_obj: + # Resize Image UV editor background image + utilities_texel.image_resize(image, int(size_B.x), int(size_B.y)) + + else: + # No Image assigned + + # Get background color from theme + 1.25x brighter + theme = bpy.context.preferences.themes[0] + color = theme.image_editor.space.back.copy() + color.r*= 1.15 + color.g*= 1.15 + color.b*= 1.15 + + image = None + if name_texture in bpy.data.images: + # TexTools Image already exists + image = bpy.data.images[name_texture] + image.scale( int(size_B.x), int(size_B.y) ) + image.generated_width = int(size_B.x) + image.generated_height = int(size_B.y) + else: + # Create new image + image = bpy.data.images.new(name_texture, width=int(size_B.x), height=int(size_B.y)) + image.generated_color = (color.r, color.g, color.b, 1.0) + image.generated_type = 'BLANK' + image.generated_width = int(size_B.x) + image.generated_height = int(size_B.y) + + # Assign in UV view + context.area.spaces.active.image = image + + # Clean up images and materials + utilities_texel.checker_images_cleanup() + + + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_uv_size_get.py b/op_uv_size_get.py new file mode 100644 index 0000000..b19a1ec --- /dev/null +++ b/op_uv_size_get.py @@ -0,0 +1,44 @@ +import bpy +import bmesh +import operator +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import utilities_texel + +class op(bpy.types.Operator): + bl_idname = "uv.textools_uv_size_get" + bl_label = "Get Size" + bl_description = "Get selected object's texture size" + + @classmethod + def poll(cls, context): + if not bpy.context.active_object: + return False + + if bpy.context.active_object not in bpy.context.selected_objects: + return False + + if bpy.context.active_object.type != 'MESH': + return False + + return True + + def execute(self, context): + get_size(self, context) + return {'FINISHED'} + + + +def get_size(self, context): + image = utilities_texel.get_object_texture_image (bpy.context.active_object) + + if not image: + self.report({'ERROR_INVALID_INPUT'}, "No Texture found on selected object" ) + return + + bpy.context.scene.texToolsSettings.size[0] = image.size[0] + bpy.context.scene.texToolsSettings.size[1] = image.size[1] + +bpy.utils.register_class(op) \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..083256b --- /dev/null +++ b/settings.py @@ -0,0 +1,17 @@ +import bpy +import bmesh +import operator + +selection_uv_mode = ''; +selection_uv_loops = [] +selection_uv_pivot = ''; +selection_uv_pivot_pos = (0,0) + +selection_mode = [False, False, True]; +selection_vert_indexies = [] +selection_face_indexies = [] + +bake_render_engine = '' +bake_objects_hide_render = [] +bake_cycles_samples = 1 +sets = [] \ No newline at end of file diff --git a/utilities_bake.py b/utilities_bake.py new file mode 100644 index 0000000..42b275c --- /dev/null +++ b/utilities_bake.py @@ -0,0 +1,605 @@ +import bpy +import bmesh +import operator +import time +from mathutils import Vector +from collections import defaultdict +from math import pi +from mathutils import Color + +from . import settings +from . import utilities_color +# from . import op_bake + + +keywords_low = ['lowpoly','low','lowp','lp','lo','l'] +keywords_high = ['highpoly','high','highp','hp','hi','h'] +keywords_cage = ['cage','c'] +keywords_float = ['floater','float','f'] + +split_chars = [' ','_','.','-'] + + + +class BakeMode: + material = "" #Material name from external blend file + type = 'EMIT' + normal_space = 'TANGENT' + setVColor = None #Set Vertex color method + color = (0.23, 0.23, 0.23, 1) #Background color + engine = 'CYCLES' #render engine, by default CYCLES + composite = None #use composite scene to process end result + use_project = False #Bake projected? + params = [] #UI Parameters from scene settings + + def __init__(self, material="", type='EMIT', normal_space='TANGENT', setVColor=None, color= (0.23, 0.23, 0.23, 1), engine='CYCLES', params = [], composite=None, use_project=False): + self.material = material + self.type = type + self.normal_space = normal_space + self.setVColor = setVColor + self.color = color + self.engine = engine + self.params = params + self.composite = composite + self.use_project = use_project + + + +def on_select_bake_mode(mode): + print("Mode changed {}".format(mode)) + + if len(settings.sets) > 0: + name_texture = "{}_{}".format(settings.sets[0].name, mode) + + if name_texture in bpy.data.images: + image = bpy.data.images[name_texture] + + # Set background image + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + area.spaces[0].image = image + + +def store_bake_settings(): + # Render Settings + settings.bake_render_engine = bpy.context.scene.render.engine + settings.bake_cycles_samples = bpy.context.scene.cycles.samples + + # Disable Objects that are meant to be hidden + sets = settings.sets + objects_sets = [] + for set in sets: + for obj in set.objects_low: + if obj not in objects_sets: + objects_sets.append(obj) + for obj in set.objects_high: + if obj not in objects_sets: + objects_sets.append(obj) + for obj in set.objects_cage: + if obj not in objects_sets: + objects_sets.append(obj) + + settings.bake_objects_hide_render = [] + + + + # for obj in bpy.context.view_layer.objects: + # if obj.hide_render == False and obj not in objects_sets: + # Check if layer is active: + # for l in range(0, len(obj.layers)): + # if obj.layers[l] and bpy.context.scene.layers[l]: + # settings.bake_objects_hide_render.append(obj) + # break #sav + + for obj in settings.bake_objects_hide_render: + obj.hide_render = True + # obj.cycles_visibility.shadow = False + + + +def restore_bake_settings(): + # Render Settings + if settings.bake_render_engine != '': + bpy.context.scene.render.engine = settings.bake_render_engine + + bpy.context.scene.cycles.samples = settings.bake_cycles_samples + + # Restore Objects that were hidden for baking + for obj in settings.bake_objects_hide_render: + if obj: + obj.hide_render = False + # obj.cycles_visibility.shadow = True + + + +stored_materials = {} +stored_material_faces = {} +def store_materials_clear(): + stored_materials.clear() + stored_material_faces.clear() + + + +def store_materials(obj): + stored_materials[obj] = [] + stored_material_faces[obj] = [] + + # Enter edit mode + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data); + + # for each slot backup the material + for s in range(len(obj.material_slots)): + slot = obj.material_slots[s] + + stored_materials[obj].append(slot.material) + stored_material_faces[obj].append( [face.index for face in bm.faces if face.material_index == s] ) + + # print("Faces: {}x".format( len(stored_material_faces[obj][-1]) )) + + if slot and slot.material: + slot.material.name = "backup_"+slot.material.name + print("- Store {} = {}".format(obj.name,slot.material.name)) + + # Back to object mode + bpy.ops.object.mode_set(mode='OBJECT') + + + +def restore_materials(): + for obj in stored_materials: + # Enter edit mode + bpy.context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data); + + # Restore slots + for index in range(len(stored_materials[obj])): + material = stored_materials[obj][index] + faces = stored_material_faces[obj][index] + + if material: + material.name = material.name.replace("backup_","") + obj.material_slots[index].material = material + + # Face material indexies + for face in bm.faces: + if face.index in faces: + face.material_index = index + + # Back to object mode + bpy.ops.object.mode_set(mode='OBJECT') + + # Remove material slots if none before + if len(stored_materials[obj]) == 0: + for i in range(len(obj.material_slots)): + bpy.ops.object.material_slot_remove() + + + +def get_set_name_base(obj): + + def remove_digits(name): + # Remove blender naming digits, e.g. cube.001, cube.002,... + if len(name)>= 4 and name[-4] == '.' and name[-3].isdigit() and name[-2].isdigit() and name[-1].isdigit(): + return name[:-4] + return name + + # Reference parent as base name + if obj.parent and obj.parent in bpy.context.selected_objects: + return remove_digits(obj.parent.name).lower() + + # Reference group name as base name + elif len(obj.users_collection) == 1: + return remove_digits(obj.users_collection[0].name).lower() + + # Use Object name + else: + return remove_digits(obj.name).lower() + + + +def get_set_name(obj): + # Get Basic name + name = get_set_name_base(obj) + + # Split by ' ','_','.' etc. + split = name.lower() + for char in split_chars: + split = split.replace(char,' ') + strings = split.split(' ') + + # Remove all keys from name + keys = keywords_cage + keywords_high + keywords_low + keywords_float + new_strings = [] + for string in strings: + is_found = False + for key in keys: + if string == key: + is_found = True + break + if not is_found: + new_strings.append(string) + elif len(new_strings) > 0: + # No more strings once key is found if we have already something + break + + return "_".join(new_strings) + + + +def get_object_type(obj): + + name = get_set_name_base(obj) + + # Detect by name pattern + split = name.lower() + for char in split_chars: + split = split.replace(char,' ') + strings = split.split(' ') + + # Detect float, more rare than low + for string in strings: + for key in keywords_float: + if key == string: + return 'float' + + # Detect by modifiers (Only if more than 1 object selected) + if len(bpy.context.selected_objects) > 1: + if obj.modifiers: + for modifier in obj.modifiers: + if modifier.type == 'SUBSURF' and modifier.render_levels > 0: + return 'high' + elif modifier.type == 'BEVEL': + return 'high' + + + # Detect High first, more rare + for string in strings: + for key in keywords_high: + if key == string: + return 'high' + + # Detect cage, more rare than low + for string in strings: + for key in keywords_cage: + if key == string: + return 'cage' + + + + # Detect low + for string in strings: + for key in keywords_low: + if key == string: + return 'low' + + + # if nothing was detected, assume its low + return 'low' + + + +def get_baked_images(sets): + images = [] + for set in sets: + name_texture = "{}_".format(set.name) + for image in bpy.data.images: + if name_texture in image.name: + images.append(image) + + return images + + + +def get_bake_sets(): + filtered = {} + for obj in bpy.context.selected_objects: + if obj.type == 'MESH': + filtered[obj] = get_object_type(obj) + + groups = [] + # Group by names + for obj in filtered: + name = get_set_name(obj) + + if len(groups)==0: + groups.append([obj]) + else: + isFound = False + for group in groups: + groupName = get_set_name(group[0]) + if name == groupName: + group.append(obj) + isFound = True + break + + if not isFound: + groups.append([obj]) + + # Sort groups alphabetically + keys = [get_set_name(group[0]) for group in groups] + keys.sort() + sorted_groups = [] + for key in keys: + for group in groups: + if key == get_set_name(group[0]): + sorted_groups.append(group) + break + + groups = sorted_groups + # print("Keys: "+", ".join(keys)) + + + bake_sets = [] + for group in groups: + low = [] + high = [] + cage = [] + float = [] + for obj in group: + if filtered[obj] == 'low': + low.append(obj) + elif filtered[obj] == 'high': + high.append(obj) + elif filtered[obj] == 'cage': + cage.append(obj) + elif filtered[obj] == 'float': + float.append(obj) + + + name = get_set_name(group[0]) + bake_sets.append(BakeSet(name, low, cage, high, float)) + + return bake_sets + + + +class BakeSet: + objects_low = [] #low poly geometry + objects_cage = [] #Cage low poly geometry + objects_high = [] #High poly geometry + objects_float = [] #Floating geometry + name = "" + + has_issues = False + + def __init__(self, name, objects_low, objects_cage, objects_high, objects_float): + self.objects_low = objects_low + self.objects_cage = objects_cage + self.objects_high = objects_high + self.objects_float = objects_float + self.name = name + + # Needs low poly objects to bake onto + if len(objects_low) == 0: + self.has_issues = True + + # Check Cage Object count to low poly count + if len(objects_cage) > 0 and (len(objects_low) != len(objects_cage)): + self.has_issues = True + + # Check for UV maps + for obj in objects_low: + if len(obj.data.uv_layers) == 0: + self.has_issues = True + break + + + +def setup_vertex_color_selection(obj): + bpy.ops.object.mode_set(mode='OBJECT') + + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + + + bpy.ops.object.mode_set(mode='VERTEX_PAINT') + + bpy.context.tool_settings.vertex_paint.brush.color = (0, 0, 0) + bpy.context.object.data.use_paint_mask = False + bpy.ops.paint.vertex_color_set() + + bpy.context.tool_settings.vertex_paint.brush.color = (1, 1, 1) + bpy.context.object.data.use_paint_mask = True + bpy.ops.paint.vertex_color_set() + + bpy.context.object.data.use_paint_mask = False + + # Back to object mode + bpy.ops.object.mode_set(mode='OBJECT') + + + +def setup_vertex_color_dirty(obj): + + print("setup_vertex_color_dirty {}".format(obj.name)) + + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT') + + # Fill white then, + bm = bmesh.from_edit_mesh(obj.data) + colorLayer = bm.loops.layers.color.verify() + + + color = utilities_color.safe_color( (1, 1, 1) ) + + for face in bm.faces: + for loop in face.loops: + loop[colorLayer] = color + obj.data.update() + + # Back to object mode + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.paint.vertex_color_dirt(dirt_angle=pi/2) + bpy.ops.paint.vertex_color_dirt() + + + +def setup_vertex_color_id_material(obj): + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + + bm = bmesh.from_edit_mesh(obj.data) + # colorLayer = bm.loops.layers.color.verify() + + for i in range(len(obj.material_slots)): + slot = obj.material_slots[i] + if slot.material: + + # Select related faces + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='DESELECT') + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + for face in bm.faces: + if face.material_index == i: + face.select = True + + color = utilities_color.get_color_id(i, len(obj.material_slots)) + + bpy.ops.object.mode_set(mode='VERTEX_PAINT') + bpy.context.tool_settings.vertex_paint.brush.color = color + bpy.context.object.data.use_paint_mask = True + bpy.ops.paint.vertex_color_set() + + obj.data.update() + + # Back to object mode + bpy.ops.object.mode_set(mode='OBJECT') + + + +def setup_vertex_color_id_element(obj): + bpy.ops.object.select_all(action='DESELECT') + obj.select_set( state = True, view_layer = None) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT') + + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + + bm = bmesh.from_edit_mesh(obj.data) + colorLayer = bm.loops.layers.color.verify() + + # Collect elements + processed = set([]) + groups = [] + for face in bm.faces: + + if face not in processed: + bpy.ops.mesh.select_all(action='DESELECT') + face.select = True + bpy.ops.mesh.select_linked(delimit={'NORMAL'}) + linked = [face for face in bm.faces if face.select] + + for link in linked: + processed.add(link) + groups.append(linked) + + # Color each group + for i in range(0,len(groups)): + color = utilities_color.get_color_id(i, len(groups)) + color = utilities_color.safe_color( color ) + for face in groups[i]: + for loop in face.loops: + loop[colorLayer] = color + obj.data.update() + + # Back to object mode + bpy.ops.object.mode_set(mode='OBJECT') + + +def get_image_material(image): + + # Claer & Create new material + material = None + if image.name in bpy.data.materials: + # Incorrect existing material, delete first and create new for cycles + material = bpy.data.materials[image.name] + material.user_clear() + bpy.data.materials.remove(material) + material = bpy.data.materials.new(image.name) + else: + material = bpy.data.materials.new(image.name) + + + # Cyles Material + if bpy.context.scene.render.engine == 'CYCLES' or bpy.context.scene.render.engine == 'BLENDER_EEVEE': + material.use_nodes = True + + # Image Node + node_image = None + if "image" in material.node_tree.nodes: + node_image = material.node_tree.nodes["image"] + else: + node_image = material.node_tree.nodes.new("ShaderNodeTexImage") + node_image.name = "image" + node_image.select = True + node_image.image = image + material.node_tree.nodes.active = node_image + + #Base Diffuse BSDF + node_diffuse = material.node_tree.nodes['Principled BSDF'] + + + if "_normal_" in image.name: + # Add Normal Map Nodes + node_normal_map = None + if "normal_map" in material.node_tree.nodes: + node_normal_map = material.node_tree.nodes["normal_map"] + else: + node_normal_map = material.node_tree.nodes.new("ShaderNodeNormalMap") + node_normal_map.name = "normal_map" + + # Tangent or World space + if(image.name.endswith("normal_tangent")): + node_normal_map.space = 'TANGENT' + elif(image.name.endswith("normal_object")): + node_normal_map.space = 'WORLD' + + # image to normal_map link + material.node_tree.links.new(node_image.outputs[0], node_normal_map.inputs[1]) + + # normal_map to diffuse_bsdf link + material.node_tree.links.new(node_normal_map.outputs[0], node_diffuse.inputs[19]) + + node_normal_map.location = node_diffuse.location - Vector((200, 0)) + node_image.location = node_normal_map.location - Vector((200, 0)) + + else: + # Other images display as Color + # dump(node_image.color_mapping.bl_rna.property_tags) + + # image node to diffuse node link + material.node_tree.links.new(node_image.outputs[0], node_diffuse.inputs[0]) + + return material + + elif bpy.context.scene.render.engine == 'BLENDER_EEVEE': + material.use_nodes = True + + texture = None + if image.name in bpy.data.textures: + texture = bpy.data.textures[image.name] + else: + texture = bpy.data.textures.new(image.name, 'IMAGE') + + texture.image = image + slot = material.texture_slot.add() + slot.texture = texture + slot.mapping = 'FLAT' + + # return material \ No newline at end of file diff --git a/utilities_color.py b/utilities_color.py new file mode 100644 index 0000000..a3eb908 --- /dev/null +++ b/utilities_color.py @@ -0,0 +1,227 @@ +import bpy +import bmesh +import operator +import time +from mathutils import Vector +from collections import defaultdict +from math import pi +from mathutils import Color + +from . import settings + + +material_prefix = "TT_color_" +gamma = 2.2 + + +def assign_slot(obj, index): + if index < len(obj.material_slots): + obj.material_slots[index].material = get_material(index) + + # Verify color + assign_color(index) + + +def safe_color(color): + if len(color) == 3: + if bpy.app.version > (2, 80, 0): + # Newer blender versions use RGBA + return (color[0], color[1], color[2], 1) + else: + return color + elif len(color) == 4: + if bpy.app.version > (2, 80, 0): + # Newer blender versions use RGBA + return color + else: + return (color[0], color[1], color[2]) + + return color + + +def assign_color(index): + material = get_material(index) + if material: + # material.use_nodes = False + + rgb = get_color(index) + rgba = (rgb[0], rgb[1], rgb[2], 1) + + if material.use_nodes and bpy.context.scene.render.engine == 'CYCLES' or material.use_nodes and bpy.context.scene.render.engine == 'BLENDER_EEVEE' : + # Cycles material (Preferred for baking) + material.node_tree.nodes["Principled BSDF"].inputs[0].default_value = rgba + material.diffuse_color = rgba + + + elif not material.use_nodes and bpy.context.scene.render.engine == 'BLENDER_EEVEE': + # Legacy render engine, not suited for baking + material.diffuse_color = rgba + + + +def get_material(index): + name = get_name(index) + + # Material already exists? + if name in bpy.data.materials: + material = bpy.data.materials[name]; + + # Check for incorrect matreials for current render engine + if not material: + replace_material(index) + + if not material.use_nodes and bpy.context.scene.render.engine == 'CYCLES': + replace_material(index) + + elif material.use_nodes and bpy.context.scene.render.engine == 'BLENDER_EEVEE': + replace_material(index) + + else: + return material; + + print("Could nt find {} , create a new one??".format(name)) + + material = create_material(index) + assign_color(index) + return material + + + +# Replaace an existing material with a new one +# This is sometimes necessary after switching the render engine +def replace_material(index): + name = get_name(index) + + print("Replace material and create new") + + # Check if material exists + if name in bpy.data.materials: + material = bpy.data.materials[name]; + + # Collect material slots we have to re-assign + slots = [] + for obj in bpy.context.view_layer.objects: + for slot in obj.material_slots: + if slot.material == material: + slots.append(slot) + + # Get new material + material.user_clear() + bpy.data.materials.remove(material) + + # Re-assign new material to all previous slots + material = create_material(index) + for slot in slots: + slot.material = material; + + + +def create_material(index): + name = get_name(index) + + # Create new image instead + material = bpy.data.materials.new(name) + material.preview_render_type = 'FLAT' + + if bpy.context.scene.render.engine == 'CYCLES': + # Cycles: prefer nodes as it simplifies baking + material.use_nodes = True + + return material + + + +def get_name(index): + return (material_prefix+"{:02d}").format(index) + + + +def get_color(index): + if index < bpy.context.scene.texToolsSettings.color_ID_count: + return getattr(bpy.context.scene.texToolsSettings, "color_ID_color_{}".format(index)) + + # Default return (Black) + return (0, 0, 0) + + + +def set_color(index, color): + if index < bpy.context.scene.texToolsSettings.color_ID_count: + setattr(bpy.context.scene.texToolsSettings, "color_ID_color_{}".format(index), color) + + + +def validate_face_colors(obj): + # Validate face colors and material slots + previous_mode = bpy.context.object.mode; + count = bpy.context.scene.texToolsSettings.color_ID_count + + # Verify enough material slots + if len(obj.material_slots) < count: + for i in range(count): + if len(obj.material_slots) < count: + bpy.ops.object.material_slot_add() + assign_slot(obj, len(obj.material_slots)-1) + else: + break + + + # TODO: Check face.material_index + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data); + for face in bm.faces: + face.material_index%= count + obj.data.update() + + # Remove material slots that are not used + if len(obj.material_slots) > count: + bpy.ops.object.mode_set(mode='OBJECT') + for i in range(len(obj.material_slots) - count): + if len(obj.material_slots) > count: + # Remove last + bpy.context.object.active_material_index = len(obj.material_slots)-1 + bpy.ops.object.material_slot_remove() + + + + + # Restore previous mode + bpy.ops.object.mode_set(mode=previous_mode) + + + +def hex_to_color(hex): + + hex = hex.strip('#') + lv = len(hex) + fin = list(int(hex[i:i + lv // 3], 16) for i in range(0, lv, lv // 3)) + r = pow(fin[0] / 255, gamma) + g = pow(fin[1] / 255, gamma) + b = pow(fin[2] / 255, gamma) + fin.clear() + fin.append(r) + fin.append(g) + fin.append(b) + return tuple(fin) + + + +def color_to_hex(color): + rgb = [] + for i in range(3): + rgb.append( pow(color[i] , 1.0/gamma) ) + + r = int(rgb[0]*255) + g = int(rgb[1]*255) + b = int(rgb[2]*255) + + return "#{:02X}{:02X}{:02X}".format(r,g,b) + + + +def get_color_id(index, count): + # Get unique color + color = Color() + color.hsv = ( index / (count) ), 0.9, 1.0 + + return color \ No newline at end of file diff --git a/utilities_meshtex.py b/utilities_meshtex.py new file mode 100644 index 0000000..47d4760 --- /dev/null +++ b/utilities_meshtex.py @@ -0,0 +1,120 @@ +import bpy +import bmesh +import operator +import time +import math +from mathutils import Vector + +# Find a mesh that contains UV mesh shape keys +def find_uv_mesh(objects, insideModifiers=True): + for obj in objects: + # Requires mesh & UV channel + if obj and obj.type == 'MESH': + + # Contains shape keys? + if obj.data.shape_keys and len(obj.data.shape_keys.key_blocks) == 2: + if "uv" in obj.data.shape_keys.key_blocks and "model" in obj.data.shape_keys.key_blocks: + return obj + + if insideModifiers: + # Find via mesh deform modifier + if len(obj.modifiers) > 0: + for modifier in obj.modifiers: + if modifier.type == 'SURFACE_DEFORM': + if modifier.target: + if modifier.target.data.shape_keys and len(modifier.target.data.shape_keys.key_blocks) == 2: + return modifier.target + return None + + + +# # Find meshes that can be wrapped aka texture meshes +def find_texture_meshes(objects): + obj_textures = [] + + for obj in objects: + if obj and obj.type == 'MESH': + if find_uv_mesh([obj], insideModifiers=False) == None: + if obj not in obj_textures: + obj_textures.append(obj) + + return obj_textures + + + +def uv_mesh_clear(obj_uv): + # Remove Previous Modifiers + if "Solidify" in obj_uv.modifiers: + obj_uv.modifiers.remove( obj_uv.modifiers["Solidify"] ) + # Remove Solidify and push modifiers + + + +def uv_mesh_fit(obj_uv, obj_textures): + # Clear first shape transition + bpy.context.scene.texToolsSettings.meshtexture_wrap = 0 + + # Clear modifiers first + uv_mesh_clear(obj_uv) + + + # Add solidify modifier + modifier_solid = obj_uv.modifiers.new(name="Solidify", type='SOLIDIFY') + modifier_solid.offset = 1 + modifier_solid.thickness = 0 #scale*0.1 #10% height + modifier_solid.use_even_offset = True + modifier_solid.thickness_clamp = 0 + modifier_solid.use_quality_normals = True + + min_z = obj_uv.location.z + max_z = obj_uv.location.z + for i in range(len(obj_textures)): + obj = obj_textures[i] + + # Min Max Z + if i == 0: + min_z = get_bbox(obj)['min'].z + max_z = get_bbox(obj)['max'].z + else: + min_z = min(min_z, get_bbox(obj)['min'].z) + max_z = max(max_z, get_bbox(obj)['max'].z) + + # Set thickness + size = max(0.1, (max_z - min_z)) + min_z-= size*0.25 #Padding + max_z+= size*0.25 + size = (max_z - min_z) + + modifier_solid.thickness = size + + # Set offset + if size > 0: + p_z = (obj_uv.location.z - min_z) / (max_z - min_z) + modifier_solid.offset = -(p_z-0.5)/0.5 + else: + modifier_solid.offset = 0 + + + +def get_bbox(obj): + corners = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box] + + # Get world space Min / Max + box_min = Vector((corners[0].x, corners[0].y, corners[0].z)) + box_max = Vector((corners[0].x, corners[0].y, corners[0].z)) + for corner in corners: + # box_min.x = -8 + box_min.x = min(box_min.x, corner.x) + box_min.y = min(box_min.y, corner.y) + box_min.z = min(box_min.z, corner.z) + + box_max.x = max(box_max.x, corner.x) + box_max.y = max(box_max.y, corner.y) + box_max.z = max(box_max.z, corner.z) + + return { + 'min':box_min, + 'max':box_max, + 'size':(box_max-box_min), + 'center':box_min+(box_max-box_min)/2 + } \ No newline at end of file diff --git a/utilities_texel.py b/utilities_texel.py new file mode 100644 index 0000000..42df878 --- /dev/null +++ b/utilities_texel.py @@ -0,0 +1,138 @@ +import bpy +import bmesh +import operator +import time +import math +from mathutils import Vector + + +image_material_prefix = "TT_checker_" + + +# Return all faces of selected objects or only selected faces +def get_selected_object_faces(): + object_faces_indexies = {} + + previous_mode = bpy.context.object.mode + + if bpy.context.object.mode == 'EDIT': + # Only selected Mesh faces + obj = bpy.context.active_object + if obj.type == 'MESH' and obj.data.uv_layers: + bm = bmesh.from_edit_mesh(obj.data) + bm.faces.ensure_lookup_table() + object_faces_indexies[obj] = [face.index for face in bm.faces if face.select] + else: + # Selected objects with all faces each + selected_objects = [obj for obj in bpy.context.selected_objects] + for obj in selected_objects: + if obj.type == 'MESH' and obj.data.uv_layers: + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set( state = True, view_layer = None) + + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + bm.faces.ensure_lookup_table() + object_faces_indexies[obj] = [face.index for face in bm.faces] + + bpy.ops.object.mode_set(mode=previous_mode) + + return object_faces_indexies + + + +def get_object_texture_image(obj): + + previous_mode = bpy.context.active_object.mode + bpy.ops.object.mode_set(mode='OBJECT') + + # Search in material & texture slots + for slot_mat in obj.material_slots: + + if slot_mat.material: + + # Check for traditional texture slots in material + for slot_tex in slot_mat.material.texture_paint_slots: + if slot_tex and slot_tex.texture and hasattr(slot_tex.texture , 'image'): + return slot_tex.texture.image + + # Check if material uses Nodes + if hasattr(slot_mat.material , 'node_tree'): + if slot_mat.material.node_tree: + for node in slot_mat.material.node_tree.nodes: + if type(node) is bpy.types.ShaderNodeTexImage: + if node.image: + return node.image + + + + return None + + + +def image_resize(image, size_x, size_y): + if image and image.source == 'FILE' or image.source == 'GENERATED': + image.generated_width = int(size_x) + image.generated_height = int(size_y) + image.scale( int(size_x), int(size_y) ) + + + +def checker_images_cleanup(): + # Clean up unused images + for image in bpy.data.images: + if image and image_material_prefix in image.name: + # Remove unused images + if not image.users: + image.user_clear() + bpy.data.images.remove(image) + return + + # Check if name missmatches size + name = get_checker_name(image.generated_type , image.size[0], image.size[1]) + if image.name != name: + # In cycles find related material as well + if image.name in bpy.data.materials: + bpy.data.materials[image.name].name = name + image.name = name + + for material in bpy.data.materials: + if material and image_material_prefix in material.name: + # Remove unused images + if not material.users: + material.user_clear() + bpy.data.materials.remove(material) + + + +def get_checker_name(mode, size_x, size_y): + return (image_material_prefix+"{1}x{2}_{0}").format(mode, size_x, size_y) + + + +def get_area_triangle_uv(A,B,C, size_x, size_y): + scale_x = size_x / max(size_x, size_y) + scale_y = size_y / max(size_x, size_y) + A.x/=scale_x + B.x/=scale_x + C.x/=scale_x + + A.y/=scale_y + B.y/=scale_y + C.y/=scale_y + + return get_area_triangle(A,B,C) + + +def get_area_triangle(A,B,C): + # Heron's formula: http://www.1728.org/triang.htm + # area = square root (s • (s - a) • (s - b) • (s - c)) + a = (B-A).length + b = (C-B).length + c = (A-C).length + s = (a+b+c)/2 + + # Use abs(s-a) for values that otherwise generate negative values e.g. pinched UV verts, otherwise math domain error + return math.sqrt(s * abs(s-a) * abs(s-b) * abs(s-c)) diff --git a/utilities_ui.py b/utilities_ui.py new file mode 100644 index 0000000..690274b --- /dev/null +++ b/utilities_ui.py @@ -0,0 +1,170 @@ +import bpy +import bpy.utils.previews +import os +from bpy.types import Panel, EnumProperty, WindowManager +from bpy.props import StringProperty + +from . import settings +from . import utilities_bake +from . import op_bake + +preview_collections = {} + +size_textures = [ + ('32', '32', ''), + ('64', '64', ''), + ('128', '128', ''), + ('256', '256', ''), + ('512', '512', ''), + ('1024', '1024', ''), + ('2048', '2048', ''), + ('4096', '4096', ''), + ('8192', '8192', '') + ] + + +preview_icons = bpy.utils.previews.new() + +def icon_get(name): + return preview_icons[name].icon_id + + +def GetContextView3D(): + for window in bpy.context.window_manager.windows: + screen = window.screen + for area in screen.areas: + if area.type == 'VIEW_3D': + for region in area.regions: + if region.type == 'WINDOW': + override = {'window': window, 'screen': screen, 'area': area, 'region': region, 'scene': bpy.context.scene, 'edit_object': bpy.context.edit_object, 'active_object': bpy.context.active_object, 'selected_objects': bpy.context.selected_objects} # Stuff the override context with very common requests by operators. MORE COULD BE NEEDED! + return override + return None + + +def GetContextViewUV(): + for window in bpy.context.window_manager.windows: + screen = window.screen + for area in screen.areas: + if area.type == 'IMAGE_EDITOR': + for region in area.regions: + if region.type == 'WINDOW': + override = {'window': window, 'screen': screen, 'area': area, 'region': region, 'scene': bpy.context.scene, 'edit_object': bpy.context.edit_object, 'active_object': bpy.context.active_object, 'selected_objects': bpy.context.selected_objects} # Stuff the override context with very common requests by operators. MORE COULD BE NEEDED! + return override + return None + + + + +def icon_register(fileName): + name = fileName.split('.')[0] # Don't include file extension + icons_dir = os.path.join(os.path.dirname(__file__), "icons") + preview_icons.load(name, os.path.join(icons_dir, fileName), 'IMAGE') + + + +def get_padding(): + size_min = min(bpy.context.scene.texToolsSettings.size[0],bpy.context.scene.texToolsSettings.size[1]) + return bpy.context.scene.texToolsSettings.padding / size_min + + + +def generate_bake_mode_previews(): + # We are accessing all of the information that we generated in the register function below + preview_collection = preview_collections["thumbnail_previews"] + image_location = preview_collection.images_location + VALID_EXTENSIONS = ('.png', '.jpg', '.jpeg') + + enum_items = [] + + # Generate the thumbnails + for i, image in enumerate(os.listdir(image_location)): + mode = image[0:-4] + print(".. .{}".format(mode)) + + + if image.endswith(VALID_EXTENSIONS) and mode in op_bake.modes: + filepath = os.path.join(image_location, image) + thumb = preview_collection.load(filepath, filepath, 'IMAGE') + enum_items.append((image, mode, "", thumb.icon_id, i)) + + return enum_items + + +def get_bake_mode(): + return str(bpy.context.scene.TT_bake_mode).replace(".png","").lower() + + +class op_popup(bpy.types.Operator): + bl_idname = "ui.textools_popup" + bl_label = "Message" + + message : StringProperty() + + def execute(self, context): + self.report({'INFO'}, self.message) + print(self.message) + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_popup(self, width=200, height=200) + + def draw(self, context): + self.layout.label(text=self.message) + + + + +def on_bakemode_set(self, context): + print("Set '{}'".format(bpy.context.scene.TT_bake_mode)) + utilities_bake.on_select_bake_mode(get_bake_mode()) + + + +def register(): + from bpy.types import Scene + from bpy.props import StringProperty, EnumProperty + + print("_______REgister previews") + + # Operators + # bpy.utils.register_class(op_popup) + + # global preview_icons + # preview_icons = bpy.utils.previews.new() + + # Create a new preview collection (only upon register) + preview_collection = bpy.utils.previews.new() + preview_collection.images_location = os.path.join(os.path.dirname(__file__), "resources/bake_modes") + preview_collections["thumbnail_previews"] = preview_collection + + + # This is an EnumProperty to hold all of the images + # You really can save it anywhere in bpy.types.* Just make sure the location makes sense + bpy.types.Scene.TT_bake_mode = EnumProperty( + items=generate_bake_mode_previews(), + update = on_bakemode_set, + default = 'normal_tangent.png' + ) + + +def unregister(): + + print("_______UNregister previews") + + from bpy.types import WindowManager + for preview_collection in preview_collections.values(): + bpy.utils.previews.remove(preview_collection) + preview_collections.clear() + + + # Unregister icons + # global preview_icons + bpy.utils.previews.remove(preview_icons) + + + del bpy.types.Scene.TT_bake_mode + +if __name__ == "__main__": + register() +bpy.utils.register_class(op_popup) \ No newline at end of file diff --git a/utilities_uv.py b/utilities_uv.py new file mode 100644 index 0000000..eb6bd87 --- /dev/null +++ b/utilities_uv.py @@ -0,0 +1,286 @@ +import bpy +import bmesh +import operator +import time +from mathutils import Vector +from collections import defaultdict +from math import pi + +from . import settings +from . import utilities_ui + +def selection_store(): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + # https://blender.stackexchange.com/questions/5781/how-to-list-all-selected-elements-in-python + # print("selectionStore") + settings.selection_uv_mode = bpy.context.scene.tool_settings.uv_select_mode + settings.selection_uv_pivot = bpy.context.tool_settings.transform_pivot_point + + settings.selection_uv_pivot_pos = bpy.context.space_data.cursor_location.copy() + + #VERT Selection + settings.selection_mode = tuple(bpy.context.scene.tool_settings.mesh_select_mode) + settings.selection_vert_indexies = [] + for vert in bm.verts: + if vert.select: + settings.selection_vert_indexies.append(vert.index) + + settings.selection_face_indexies = [] + for face in bm.faces: + if face.select: + settings.selection_face_indexies.append(face.index) + + + #Face selections (Loops) + settings.selection_uv_loops = [] + for face in bm.faces: + for loop in face.loops: + if loop[uv_layers].select: + settings.selection_uv_loops.append( [face.index, loop.vert.index] ) + + + +def selection_restore(bm = None, uv_layers = None): + + if bpy.context.object.mode != 'EDIT': + bpy.ops.object.mode_set(mode = 'EDIT') + + if not bm: + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + if not uv_layers: + uv_layers = bm.loops.layers.uv.verify(); + + # print("selectionRestore") + bpy.context.scene.tool_settings.uv_select_mode = settings.selection_uv_mode + bpy.context.tool_settings.transform_pivot_point = settings.selection_uv_pivot + + contextViewUV = utilities_ui.GetContextViewUV() + if contextViewUV: + bpy.ops.uv.cursor_set(contextViewUV, location=settings.selection_uv_pivot_pos) + + + bpy.ops.mesh.select_all(action='DESELECT') + + if hasattr(bm.verts, "ensure_lookup_table"): + bm.verts.ensure_lookup_table() + # bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + #FACE selection + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') + for index in settings.selection_face_indexies: + if index < len(bm.faces): + bm.faces[index].select = True + + #VERT Selection + bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT') + for index in settings.selection_vert_indexies: + if index < len(bm.verts): + bm.verts[index].select = True + + #Selection Mode + bpy.context.scene.tool_settings.mesh_select_mode = settings.selection_mode + + + #UV Face-UV Selections (Loops) + bpy.ops.uv.select_all(contextViewUV, action='DESELECT') + for uv_set in settings.selection_uv_loops: + for loop in bm.faces[ uv_set[0] ].loops: + if loop.vert.index == uv_set[1]: + loop[uv_layers].select = True + break + + bpy.context.view_layer.update() + + + +def get_selected_faces(): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + faces = []; + for face in bm.faces: + if face.select: + faces.append(face) + + return faces + + + +def set_selected_faces(faces): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + for face in faces: + for loop in face.loops: + loop[uv_layers].select = True + + +def get_selected_uvs(bm, uv_layers): + """Returns selected mesh vertices of selected UV's""" + uvs = [] + for face in bm.faces: + if face.select: + for loop in face.loops: + if loop[uv_layers].select: + uvs.append( loop[uv_layers] ) + return uvs + + + +def get_selected_uv_verts(bm, uv_layers): + """Returns selected mesh vertices of selected UV's""" + verts = set() + for face in bm.faces: + if face.select: + for loop in face.loops: + if loop[uv_layers].select: + verts.add( loop.vert ) + return list(verts) + + + +def get_selected_uv_edges(bm, uv_layers): + """Returns selected mesh edges of selected UV's""" + verts = get_selected_uv_verts(bm, uv_layers) + edges = [] + for edge in bm.edges: + if edge.verts[0] in verts and edge.verts[1] in verts: + edges.append(edge) + return edges + + + +def get_selected_uv_faces(bm, uv_layers): + """Returns selected mesh faces of selected UV's""" + faces = [] + for face in bm.faces: + if face.select: + count = 0 + for loop in face.loops: + if loop[uv_layers].select: + count+=1 + if count == len(face.loops): + faces.append(face) + return faces + + + +def get_vert_to_uv(bm, uv_layers): + vert_to_uv = {} + for face in bm.faces: + for loop in face.loops: + vert = loop.vert + uv = loop[uv_layers] + if vert not in vert_to_uv: + vert_to_uv[vert] = [uv]; + else: + vert_to_uv[vert].append(uv) + return vert_to_uv + + + +def get_uv_to_vert(bm, uv_layers): + uv_to_vert = {} + for face in bm.faces: + for loop in face.loops: + vert = loop.vert + uv = loop[uv_layers] + if uv not in uv_to_vert: + uv_to_vert[ uv ] = vert; + return uv_to_vert + + + + +def getSelectionBBox(): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data); + uv_layers = bm.loops.layers.uv.verify(); + + bbox = {} + + boundsMin = Vector((99999999.0,99999999.0)) + boundsMax = Vector((-99999999.0,-99999999.0)) + boundsCenter = Vector((0.0,0.0)) + countFaces = 0; + + for face in bm.faces: + if face.select: + for loop in face.loops: + if loop[uv_layers].select is True: + uv = loop[uv_layers].uv + boundsMin.x = min(boundsMin.x, uv.x) + boundsMin.y = min(boundsMin.y, uv.y) + boundsMax.x = max(boundsMax.x, uv.x) + boundsMax.y = max(boundsMax.y, uv.y) + + boundsCenter+= uv + countFaces+=1 + + bbox['min'] = boundsMin + bbox['max'] = boundsMax + bbox['width'] = (boundsMax - boundsMin).x + bbox['height'] = (boundsMax - boundsMin).y + + if countFaces == 0: + bbox['center'] = boundsMin + else: + bbox['center'] = boundsCenter / countFaces + + bbox['area'] = bbox['width'] * bbox['height'] + bbox['minLength'] = min(bbox['width'], bbox['height']) + + return bbox; + + + +def getSelectionIslands(bm=None, uv_layers=None): + if bm == None: + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + #Reference A: https://github.com/nutti/Magic-UV/issues/41 + #Reference B: https://github.com/c30ra/uv-align-distribute/blob/v2.2/make_island.py + + #Extend selection + if bpy.context.scene.tool_settings.use_uv_select_sync == False: + bpy.ops.uv.select_linked() + + #Collect selected UV faces + faces_selected = []; + for face in bm.faces: + if face.select and face.loops[0][uv_layers].select: + faces_selected.append(face) + + #Collect UV islands + # faces_parsed = [] + faces_unparsed = faces_selected.copy() + islands = [] + + for face in faces_selected: + if face in faces_unparsed: + + #Select single face + bpy.ops.uv.select_all(action='DESELECT') + face.loops[0][uv_layers].select = True; + bpy.ops.uv.select_linked()#Extend selection + + #Collect faces + islandFaces = [face]; + for faceAll in faces_unparsed: + if faceAll != face and faceAll.select and faceAll.loops[0][uv_layers].select: + islandFaces.append(faceAll) + + for faceAll in islandFaces: + faces_unparsed.remove(faceAll) + + #Assign Faces to island + islands.append(islandFaces) + + #Restore selection + # for face in faces_selected: + # for loop in face.loops: + # loop[uv_layers].select = True + + + print("Islands: {}x".format(len(islands))) + return islands