Port to Blender 2.80
24
LICENSE.txt
Normal file
@ -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
|
1475
__init__.py
Normal file
BIN
bake_anti_alias.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
bake_obj_cage.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
bake_obj_float.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
bake_obj_high.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
bake_obj_low.png
Normal file
After Width: | Height: | Size: 16 KiB |
131
op_align.py
Normal file
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
BIN
op_align_bottom.png
Normal file
After Width: | Height: | Size: 604 B |
BIN
op_align_left.png
Normal file
After Width: | Height: | Size: 587 B |
BIN
op_align_right.png
Normal file
After Width: | Height: | Size: 608 B |
BIN
op_align_top.png
Normal file
After Width: | Height: | Size: 575 B |
BIN
op_bake.png
Normal file
After Width: | Height: | Size: 16 KiB |
581
op_bake.py
Normal file
@ -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)
|
BIN
op_bake_explode.png
Normal file
After Width: | Height: | Size: 16 KiB |
221
op_bake_explode.py
Normal file
@ -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)
|
179
op_bake_organize_names.py
Normal file
@ -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)
|
||||
|
98
op_color_assign.py
Normal file
@ -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)
|
96
op_color_clear.py
Normal file
@ -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)
|
BIN
op_color_convert_texture.png
Normal file
After Width: | Height: | Size: 15 KiB |
153
op_color_convert_texture.py
Normal file
@ -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)
|
BIN
op_color_convert_vertex_colors.png
Normal file
After Width: | Height: | Size: 15 KiB |
90
op_color_convert_vertex_colors.py
Normal file
@ -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)
|
BIN
op_color_from_directions.png
Normal file
After Width: | Height: | Size: 16 KiB |
206
op_color_from_directions.py
Normal file
@ -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)
|
102
op_color_from_elements.py
Normal file
@ -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)
|
53
op_color_from_materials.py
Normal file
@ -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)
|
42
op_color_io_export.py
Normal file
@ -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)
|
66
op_color_io_import.py
Normal file
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
69
op_color_select.py
Normal file
@ -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)
|
375
op_edge_split_bevel.py
Normal file
@ -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)
|
142
op_island_align_edge.py
Normal file
@ -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)
|
171
op_island_align_sort.py
Normal file
@ -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)
|
291
op_island_align_world.py
Normal file
@ -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)
|
783
op_island_mirror.py
Normal file
@ -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)
|
81
op_island_rotate_90.py
Normal file
@ -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)
|
272
op_island_straighten_edge_loops.py
Normal file
@ -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)
|
||||
|
||||
|
296
op_meshtex_create.py
Normal file
@ -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)
|
193
op_meshtex_pattern.py
Normal file
@ -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)
|
||||
|
77
op_meshtex_trim.py
Normal file
@ -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)
|
||||
|
||||
|
70
op_meshtex_trim_collapse.py
Normal file
@ -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)
|
86
op_meshtex_wrap.py
Normal file
@ -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)
|
637
op_rectify.py
Normal file
@ -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)
|
133
op_select_islands_flipped.py
Normal file
@ -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)
|
130
op_select_islands_identical.py
Normal file
@ -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)
|
78
op_select_islands_outline.py
Normal file
@ -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)
|
140
op_select_islands_overlap.py
Normal file
@ -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)
|
67
op_smoothing_uv_islands.py
Normal file
@ -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)
|
250
op_texel_checker_map.py
Normal file
@ -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)
|
133
op_texel_density_get.py
Normal file
@ -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)
|
||||
|
189
op_texel_density_set.py
Normal file
@ -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)
|
50
op_texture_open.py
Normal file
@ -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)
|
101
op_texture_preview.py
Normal file
@ -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)
|
64
op_texture_reload_all.py
Normal file
@ -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)
|
43
op_texture_remove.py
Normal file
@ -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)
|
109
op_texture_save.py
Normal file
@ -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)
|
84
op_texture_select.py
Normal file
@ -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)
|
95
op_unwrap_edge_peel.py
Normal file
@ -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)
|
76
op_unwrap_faces_iron.py
Normal file
@ -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)
|
67
op_uv_channel_add.py
Normal file
@ -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)
|
70
op_uv_channel_swap.py
Normal file
@ -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)
|
66
op_uv_crop.py
Normal file
@ -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)
|
117
op_uv_fill.py
Normal file
@ -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)
|
264
op_uv_resize.py
Normal file
@ -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)
|
44
op_uv_size_get.py
Normal file
@ -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)
|
17
settings.py
Normal file
@ -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 = []
|
605
utilities_bake.py
Normal file
@ -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
|
227
utilities_color.py
Normal file
@ -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
|
120
utilities_meshtex.py
Normal file
@ -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
|
||||
}
|
138
utilities_texel.py
Normal file
@ -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))
|
170
utilities_ui.py
Normal file
@ -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)
|
286
utilities_uv.py
Normal file
@ -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
|