Port to Blender 2.80

pull/1/head
SavMartin 5 years ago committed by GitHub
parent 12b2cdc147
commit 8e579918f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,24 @@
# Blender TexTools,
# <TexTools, Blender addon for editing UVs and Texture maps.>
# Copyright (C) <2018> <renderhjs>
# 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 <http://www.gnu.org/licenses/>.
# Credits
#
# UVSquares:
# <Uv Squares, Blender addon for reshaping UV vertices to grid.>
# Copyright (C) <2014> <Reslav Hollos>
# https://github.com/JoseConseco/UvSquares/blob/master/uv_squares.py

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -0,0 +1,221 @@
import bpy
import bmesh
import operator
from mathutils import Vector
from collections import defaultdict
from math import pi
from . import settings
frame_range = 50
class op(bpy.types.Operator):
bl_idname = "uv.textools_bake_explode"
bl_label = "Explode"
bl_description = "Explode selected bake pairs with animation keyframes"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if len(settings.sets) <= 1:
return False
return True
def execute(self, context):
explode(self)
return {'FINISHED'}
def explode(self):
sets = settings.sets
set_bounds = {}
set_volume = {}
avg_side = 0
for set in sets:
set_bounds[set] = get_bbox_set(set)
set_volume[set] = set_bounds[set]['size'].x * set_bounds[set]['size'].y * set_bounds[set]['size'].z
avg_side+=set_bounds[set]['size'].x
avg_side+=set_bounds[set]['size'].y
avg_side+=set_bounds[set]['size'].z
avg_side/=(len(sets)*3)
sorted_set_volume = sorted(set_volume.items(), key=operator.itemgetter(1))
sorted_sets = [item[0] for item in sorted_set_volume]
sorted_sets.reverse()
# All combined bounding boxes
bbox_all = merge_bounds(list(set_bounds.values()))
bbox_max = set_bounds[ sorted_sets[0] ] # max_bbox(list(set_bounds.values()))
# Offset sets into their direction
dir_offset_last_bbox = {}
for i in range(0,6):
dir_offset_last_bbox[i] = bbox_max #bbox_all
bpy.context.scene.frame_start = 0
bpy.context.scene.frame_end = frame_range
bpy.context.scene.frame_current = 0
# Process each set
for set in sorted_sets:
if set_bounds[set] != bbox_max:
delta = set_bounds[set]['center'] - bbox_all['center']
offset_set(set, delta, avg_side*0.35, dir_offset_last_bbox )
def offset_set(set, delta, margin, dir_offset_last_bbox):
objects = set.objects_low + set.objects_high + set.objects_cage
# print("\nSet '{}' with {}x".format(set.name, len(objects) ))
# Which Direction?
delta_max = max(abs(delta.x), abs(delta.y), abs(delta.z))
direction = [0,0,0]
if delta_max > 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)

@ -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)

@ -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)

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1,90 @@
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
gamma = 2.2
class op(bpy.types.Operator):
bl_idname = "uv.textools_color_convert_to_vertex_colors"
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):
convert_vertex_colors(self, context)
return {'FINISHED'}
def convert_vertex_colors(self, context):
obj = bpy.context.active_object
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(i).copy()
# Fix Gamma
color[0] = pow(color[0],1/gamma)
color[1] = pow(color[1],1/gamma)
color[2] = pow(color[2],1/gamma)
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()
# Back to object mode
bpy.ops.object.mode_set(mode='VERTEX_PAINT')
bpy.context.object.data.use_paint_mask = False
# 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'
# Clear any materials
bpy.ops.uv.textools_color_clear()
bpy.ops.ui.textools_popup('INVOKE_DEFAULT', message="Vertex colors assigned")
bpy.utils.register_class(op)

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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)

@ -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 = []

@ -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

@ -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

@ -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
}

@ -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))

@ -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)

@ -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
Loading…
Cancel
Save