2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
@@ -0,0 +1,197 @@
import bmesh
from bpy.types import Operator
from bpy.props import EnumProperty
from ..utils.uv_utils import get_islands, get_bbox, get_objects_seams
class AlignUv(Operator):
bl_idname = "uv.toolkit_align_uv"
bl_label = "Align UV"
bl_description = "Align Verts or UV Islands"
bl_options = {'REGISTER', 'UNDO'}
align_uv: EnumProperty(
name='Axis',
items=[
('MAX_U', 'Max X', ''),
('MIN_U', 'Min X', ''),
('MAX_V', 'Max Y', ''),
('MIN_V', 'Min Y', ''),
],
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def align_islands(self, context):
bboxes = []
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
bbox = get_bbox(uv, island)
bboxes.append(bbox)
if not bboxes:
return {'CANCELED'}
if self.align_uv == 'MAX_U':
max_u = max([bbox[1][0] for bbox in bboxes])
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
bbox = get_bbox(uv, island)
bbox_max_u = bbox[1][0]
distance = max_u - bbox_max_u
for f in island:
for l in f.loops:
u, v = l[uv].uv
new_co = u + distance, v
l[uv].uv = new_co
bmesh.update_edit_mesh(me)
if self.align_uv == 'MIN_U':
min_u = min([bbox[0][0] for bbox in bboxes])
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
bbox = get_bbox(uv, island)
bbox_min_u = bbox[0][0]
distance = bbox_min_u - min_u
for f in island:
for l in f.loops:
u, v = l[uv].uv
new_co = u - distance, v
l[uv].uv = new_co
bmesh.update_edit_mesh(me)
if self.align_uv == 'MAX_V':
max_v = max([bbox[1][1] for bbox in bboxes])
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
bbox = get_bbox(uv, island)
bbox_max_v = bbox[1][1]
distance = max_v - bbox_max_v
for f in island:
for l in f.loops:
u, v = l[uv].uv
new_co = u, v + distance
l[uv].uv = new_co
bmesh.update_edit_mesh(me)
if self.align_uv == 'MIN_V':
min_v = min([bbox[0][1] for bbox in bboxes])
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
bbox = get_bbox(uv, island)
bbox_min_v = bbox[0][1]
distance = bbox_min_v - min_v
for f in island:
for l in f.loops:
u, v = l[uv].uv
new_co = u, v - distance
l[uv].uv = new_co
bmesh.update_edit_mesh(me)
def align_vertices(self, context):
coords = []
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for f in bm.faces:
if f.select:
for l in f.loops:
if l[uv].select:
coords.append(l[uv].uv[:])
if not coords:
return {'CANCELED'}
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
if self.align_uv == 'MAX_U':
u = max([uv[0] for uv in coords])
for f in bm.faces:
if f.select:
for l in f.loops:
if l[uv].select:
for l in l.vert.link_loops:
if l[uv].select:
l[uv].uv[0] = u
if self.align_uv == 'MIN_U':
u = min([uv[0] for uv in coords])
for f in bm.faces:
if f.select:
for l in f.loops:
if l[uv].select:
for l in l.vert.link_loops:
if l[uv].select:
l[uv].uv[0] = u
if self.align_uv == 'MAX_V':
v = max([uv[1] for uv in coords])
for f in bm.faces:
if f.select:
for l in f.loops:
if l[uv].select:
for l in l.vert.link_loops:
if l[uv].select:
l[uv].uv[1] = v
if self.align_uv == 'MIN_V':
v = min([uv[1] for uv in coords])
for f in bm.faces:
if f.select:
for l in f.loops:
if l[uv].select:
for l in l.vert.link_loops:
if l[uv].select:
l[uv].uv[1] = v
bmesh.update_edit_mesh(me)
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
if scene.uv_toolkit.align_mode == 'VERTICES':
self.align_vertices(context)
else:
self.align_islands(context)
return {'FINISHED'}
def draw(self, context):
scene = context.scene
layout = self.layout
row = layout.row()
row.label(text="")
row.prop(scene.uv_toolkit, "align_mode", expand=True)
layout.use_property_split = True
layout.prop(self, "align_uv", expand=True)
@@ -0,0 +1,20 @@
import bpy
class BorderSeam(bpy.types.Operator):
bl_idname = "uv.toolkit_border_seam"
bl_label = "Border Seam"
bl_description = "Mark seams around the selection border"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
tool_settings = context.tool_settings
current_select_mode = tool_settings.mesh_select_mode[:]
bpy.ops.mesh.region_to_loop()
bpy.ops.mesh.mark_seam(clear=False)
tool_settings.mesh_select_mode = current_select_mode
return {'FINISHED'}
@@ -0,0 +1,14 @@
import bpy
class CenterCursorFrameAll(bpy.types.Operator):
bl_idname = "uv.toolkit_center_cursor_and_frame_all"
bl_label = "Center Cursor and Frame All"
bl_description = " "
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
space_data = context.space_data
space_data.cursor_location = 0, 0
bpy.ops.image.view_all()
return {'FINISHED'}
@@ -0,0 +1,21 @@
import bmesh
from bpy.types import Operator
class ClearAllPins(Operator):
bl_idname = "uv.toolkit_clear_all_pins"
bl_label = "Clear All Pins"
bl_description = " "
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for f in bm.faces:
for l in f.loops:
l[uv].pin_uv = False
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,22 @@
from bpy.types import Operator
import bmesh
from ..utils.uv_utils import clear_all_seams
class ClearAllSeams(Operator):
bl_idname = "uv.toolkit_clear_all_seams"
bl_label = "Clear All Seams"
bl_description = "Clear all seams of objects in edit mode"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
clear_all_seams(bm)
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,177 @@
import os
from collections import defaultdict
import bpy
import bmesh
from bpy.props import IntProperty, StringProperty
from ..functions import get_addon_preferences
class CheckerMaterial(bpy.types.Operator):
bl_idname = "uv.toolkit_create_checker_material"
bl_label = "Create Checker Material"
bl_description = "Create and assign checker material for the selected objects"
bl_options = {'REGISTER', 'INTERNAL', 'UNDO'}
width: IntProperty(options={'HIDDEN'})
height: IntProperty(options={'HIDDEN'})
checker_image_path: StringProperty(options={'HIDDEN'})
def get_uv_checker_map(self, _context):
addon_prefs = get_addon_preferences()
checker_type = addon_prefs.checker_type
if addon_prefs.checker_map == "BUILT-IN":
if checker_type in ('UV_GRID', 'COLOR_GRID'):
img_size = f"{self.width}x{self.height}"
checker_name = \
f"uv_checker_map_{str.lower(checker_type)}_{img_size}"
if not bpy.data.images.get(checker_name):
bpy.ops.image.new(
name=checker_name,
width=self.width,
height=self.height,
generated_type=checker_type
)
else:
img_name = os.path.basename(self.checker_image_path)
checker_name = f"uv_checker_map_custom_{img_name}"
if len(checker_name) > 63:
checker_name = checker_name[0:63]
if bpy.data.images.get(checker_name) is None:
img = bpy.data.images.load(self.checker_image_path)
img.name = checker_name
return bpy.data.images[checker_name]
def get_uv_checker_material(self, _context, uv_checker_map):
checker_name = uv_checker_map.name
material_name = checker_name.replace("uv_checker_map",
"uv_checker_material")
if len(material_name) > 63:
material_name = material_name[0:63]
if bpy.data.materials.get(material_name) is None:
mat = bpy.data.materials.new(material_name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
nodes.remove(nodes["Principled BSDF"])
node_texture = nodes.new(type="ShaderNodeTexImage")
node_texture.image = bpy.data.images[checker_name]
node_texture.location = -20, 300
links = mat.node_tree.links
links.new(node_texture.outputs[0],
nodes.get("Material Output").inputs[0])
return bpy.data.materials.get(material_name)
def backup_active_material(self, context, ob, bm, multiple_materials):
if multiple_materials:
materials_faces = defaultdict(list)
material_idx_layer = bm.faces.layers.int.get("material_idx_layer")
if not material_idx_layer:
material_idx_layer = bm.faces.layers.int.new("material_idx_layer")
for f in bm.faces:
materials_faces[f.material_index].append(f)
for material_index in materials_faces:
for f in materials_faces[material_index]:
f[material_idx_layer] = material_index
else:
if ob.material_slots:
if ob.data.polygons:
material_index = ob.data.polygons[0].material_index
ob.active_material_index = material_index
material = ob.active_material
if material and not material.name.startswith("uv_checker_material"):
context.object.active_material.use_fake_user = True
ob["uv_toolkit_init_material"] = material.name
def set_viewport_shading(self, context):
workspace = context.workspace
current_workspace = workspace.screens[0].areas
for area in current_workspace:
for space in area.spaces:
if space.type == 'VIEW_3D':
if space.shading.type == 'WIREFRAME':
space.shading.type = 'SOLID'
space.shading.color_type = 'TEXTURE'
def assign_image_in_uv_editor(self, context, uv_checker_map, addon_prefs):
screen = context.screen
for area in screen.areas:
if area.type == 'IMAGE_EDITOR':
area.spaces.active.image = uv_checker_map
if addon_prefs.assign_image_in_uv_editor == 'DISABLE':
area.spaces.active.image = None
def get_multiple_materials(self, _context, bm):
if bm.faces:
current_material_index = bm.faces[0].material_index
for f in bm.faces:
if f.material_index != current_material_index:
return True
def assign_checker_material(self, context, ob, bm, uv_checker_map, multiple_materials):
material = self.get_uv_checker_material(context, uv_checker_map)
if "uv_toolkit_multiple_materials" in ob:
for index, slot in enumerate(ob.material_slots):
if slot.material:
if slot.material.name.startswith("uv_checker_material"):
break
ob.active_material_index = index
for f in bm.faces:
f.material_index = index
ob.active_material = material
context.object.active_material.use_fake_user = True
ob["uv_toolkit_multiple_materials"] = 1
return
if multiple_materials:
# print("Multiple mats")
bpy.ops.object.material_slot_add()
index = ob.active_material_index
for f in bm.faces:
f.material_index = index
ob["uv_toolkit_multiple_materials"] = 1
else:
# print("Single mat")
if bm.faces:
material_index = bm.faces[0].material_index
ob.active_material_index = material_index
ob["uv_toolkit_checker_material"] = material.name
ob.active_material = material
context.object.active_material.use_fake_user = True
def execute(self, context):
if not context.selected_objects:
self.report({'WARNING'}, 'No Objects Selected')
return {'CANCELLED'}
addon_prefs = get_addon_preferences()
uv_checker_map = self.get_uv_checker_map(context)
self.set_viewport_shading(context)
view_layer = context.view_layer
act_ob = view_layer.objects.active
selected_objectes = [ob for ob in context.selected_objects if ob.type == 'MESH']
if selected_objectes:
view_layer.objects.active = selected_objectes[0]
initial_mode = context.active_object.mode
if initial_mode != 'EDIT':
bpy.ops.object.mode_set(mode='EDIT')
for ob in selected_objectes:
view_layer.objects.active = ob
me = ob.data
bm = bmesh.from_edit_mesh(me)
bm.faces.ensure_lookup_table()
multiple_materials = self.get_multiple_materials(context, bm)
self.backup_active_material(context, ob, bm, multiple_materials)
self.assign_checker_material(context, ob, bm, uv_checker_map, multiple_materials)
bpy.ops.object.mode_set(mode=initial_mode)
view_layer.objects.active = act_ob
self.assign_image_in_uv_editor(context, uv_checker_map, addon_prefs)
return {'FINISHED'}
@@ -0,0 +1,23 @@
from bpy.types import Operator
from bpy.props import BoolProperty
class CreateNewUvLayer(Operator):
bl_idname = "uv.toolkit_create_new_uv_layer"
bl_label = "Create new UV layer"
bl_description = "Create new UV layer for selected objects"
bl_options = {'REGISTER', 'UNDO'}
set_active: BoolProperty(
name="Set Active",
default=True
)
def execute(self, context):
scene = context.scene
for ob in context.selected_objects:
if ob.type == "MESH":
ob.data.uv_layers.new(name=scene.uv_toolkit.uv_layer_name)
if self.set_active:
ob.data.uv_layers.active_index = len(ob.data.uv_layers) - 1
return {'FINISHED'}
@@ -0,0 +1,42 @@
from bpy.types import Operator
from bpy.props import EnumProperty
class DeleteUvLayer(Operator):
bl_idname = "uv.toolkit_delete_uv_layer"
bl_label = "Delete UV layer"
bl_description = "Delete UV layer for selected objects"
bl_options = {'REGISTER', 'UNDO'}
mode: EnumProperty(
name="Mode",
items=[
("INDEX", "Index", ""),
("NAME", "Name", "")
],
)
def execute(self, context):
scene = context.scene
ob = context.active_object
scene_layer_index = scene.uv_toolkit.uv_layer_index
for ob in context.selected_objects:
if ob.type == "MESH":
if self.mode == "INDEX":
if len(ob.data.uv_layers) >= scene_layer_index:
ob.data.uv_layers.remove(ob.data.uv_layers[scene_layer_index - 1])
if self.mode == "NAME":
for uv_layer in ob.data.uv_layers:
if uv_layer.name == scene.uv_toolkit.uv_layer_name:
ob.data.uv_layers.remove(uv_layer)
break
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, "mode", expand=True)
@@ -0,0 +1,286 @@
from math import sin, cos
import bmesh
from bpy.types import Operator
from bpy.props import BoolProperty
from ..utils.uv_utils import (
calc_slope,
calc_length_sorted_uv_edges,
get_sorted_uv_edge_loops,
find_nearest_axis,
get_direction,
)
AXIS_U = 0
AXIS_V = 1
NEGATIVE = 0
POSITIVE = 1
class Distribute(Operator):
bl_idname = "uv.toolkit_distribute"
bl_label = "Distribute"
bl_description = "Align the selected vertices evenly or keeping edges length"
bl_options = {'REGISTER', 'UNDO'}
preserve_edge_length: BoolProperty(
name="Preserve length",
default=False
)
align_to_nearest_axis: BoolProperty(
name="Align",
default=False
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def distribute(
self, uv, first_uv_vert, end_uv_vert,
first_uv_vert_co, end_uv_vert_co,
u1, v1, u2, v2, slope, number_uv_edges,
uv_edge_loop):
if self.align_to_nearest_axis or slope == 0:
axis = find_nearest_axis(slope,
first_uv_vert_co.to_tuple(4),
end_uv_vert_co.to_tuple(4))
direction = get_direction(axis, first_uv_vert_co, end_uv_vert_co)
if axis == AXIS_U:
uv_edge_length = abs((u1 - u2)) / number_uv_edges
avg_v = v1 + ((v2 - v1) / 2)
first_uv_vert_co = (u1, avg_v)
if axis == AXIS_V:
uv_edge_length = abs((v1 - v2)) / number_uv_edges
avg_u = u1 + ((u2 - u1) / 2)
first_uv_vert_co = (avg_u, v1)
for l in first_uv_vert:
l[uv].uv = first_uv_vert_co
# Apply uv coordinates.
for idx in range(number_uv_edges):
current_uv_vert = uv_edge_loop[idx]
next_uv_vert = uv_edge_loop[idx + 1]
u1 = current_uv_vert[0][uv].uv[0]
v1 = current_uv_vert[0][uv].uv[1]
if axis == AXIS_U:
if direction == POSITIVE:
new_co = (u1 + uv_edge_length, avg_v)
if direction == NEGATIVE:
new_co = (u1 - uv_edge_length, avg_v)
if axis == AXIS_V:
if direction == POSITIVE:
new_co = (avg_u, v1 + uv_edge_length)
if direction == NEGATIVE:
new_co = (avg_u, v1 - uv_edge_length)
for l in next_uv_vert:
l[uv].uv = new_co
else:
uv_edge_length = (
(first_uv_vert_co - end_uv_vert_co).length / (number_uv_edges))
if v1 < v2:
start_uv_vert = 'BOTTOM'
else:
start_uv_vert = 'TOP'
if u1 < u2:
direction = POSITIVE
else:
direction = NEGATIVE
u_projection = uv_edge_length * cos(slope)
v_projection = uv_edge_length * sin(slope)
# Apply uv coordinates.
for idx in range(number_uv_edges):
current_uv_vert = uv_edge_loop[idx]
next_uv_vert = uv_edge_loop[idx + 1]
u1 = current_uv_vert[0][uv].uv[0]
v1 = current_uv_vert[0][uv].uv[1]
if start_uv_vert == 'BOTTOM':
if direction == POSITIVE:
new_co = (u1 + u_projection, v1 + v_projection)
if direction == NEGATIVE:
new_co = (u1 - u_projection, v1 - v_projection)
if start_uv_vert == 'TOP':
if direction == POSITIVE:
new_co = (u1 + u_projection, v1 + v_projection)
if direction == NEGATIVE:
new_co = (u1 - u_projection, v1 - v_projection)
for l in next_uv_vert:
l[uv].uv = new_co
def distribute_preserve_edges_length(
self, uv, first_uv_vert, end_uv_vert,
first_uv_vert_co, end_uv_vert_co,
u1, v1, u2, v2, slope, number_uv_edges,
uv_edge_loop):
uv_edges_length = calc_length_sorted_uv_edges(uv, uv_edge_loop)
all_edges_length = sum(uv_edges_length)
if self.align_to_nearest_axis or slope == 0:
axis = find_nearest_axis(slope,
first_uv_vert_co.to_tuple(4),
end_uv_vert_co.to_tuple(4))
direction = get_direction(axis, first_uv_vert_co, end_uv_vert_co)
if axis == AXIS_U:
offset = (all_edges_length - abs(u2 - u1)) / 2
avg_v = v1 + ((v2 - v1) / 2)
if direction == POSITIVE:
first_uv_vert_co = (u1 - offset, avg_v)
if direction == NEGATIVE:
first_uv_vert_co = (u1 + offset, avg_v)
if axis == AXIS_V:
offset = (all_edges_length - abs(v2 - v1)) / 2
avg_u = u1 + ((u2 - u1) / 2)
if direction == POSITIVE:
first_uv_vert_co = (avg_u, v1 - offset)
if direction == NEGATIVE:
first_uv_vert_co = (avg_u, v1 + offset)
for l in first_uv_vert:
l[uv].uv = first_uv_vert_co
# Apply uv coordinates.
for idx in range(number_uv_edges):
current_uv_vert = uv_edge_loop[idx]
next_uv_vert = uv_edge_loop[idx + 1]
edge_length = uv_edges_length[idx]
u1 = current_uv_vert[0][uv].uv[0]
v1 = current_uv_vert[0][uv].uv[1]
if axis == AXIS_U:
if direction == POSITIVE:
new_co = (u1 + edge_length, v1)
if direction == NEGATIVE:
new_co = (u1 - edge_length, v1)
if axis == AXIS_V:
if direction == POSITIVE:
new_co = (u1, v1 + edge_length)
if direction == NEGATIVE:
new_co = (u1, v1 - edge_length)
for l in next_uv_vert:
l[uv].uv = new_co
else:
initial_length = (first_uv_vert_co - end_uv_vert_co).length
offset = (all_edges_length - initial_length) / 2
u_projection = offset * cos(slope)
v_projection = offset * sin(slope)
if v1 < v2:
start_uv_vert = 'BOTTOM'
else:
start_uv_vert = 'TOP'
if u1 < u2:
direction = POSITIVE
else:
direction = NEGATIVE
# Tweak start vert.
if start_uv_vert == 'BOTTOM':
if direction == POSITIVE:
new_co = (u1 - u_projection, v1 - v_projection)
if direction == NEGATIVE:
new_co = (u1 + u_projection, v1 + v_projection)
if start_uv_vert == 'TOP':
if direction == POSITIVE:
new_co = (u1 - u_projection, v1 - v_projection)
if direction == NEGATIVE:
new_co = (u1 + u_projection, v1 + v_projection)
for l in first_uv_vert:
l[uv].uv = new_co
# Apply uv coordinates.
for idx in range(number_uv_edges):
current_uv_vert = uv_edge_loop[idx]
next_uv_vert = uv_edge_loop[idx + 1]
edge_length = uv_edges_length[idx]
u1 = current_uv_vert[0][uv].uv[0]
v1 = current_uv_vert[0][uv].uv[1]
u_projection = edge_length * cos(slope)
v_projection = edge_length * sin(slope)
if start_uv_vert == 'BOTTOM':
if direction == POSITIVE:
new_co = (u1 + u_projection, v1 + v_projection)
if direction == NEGATIVE:
new_co = (u1 - u_projection, v1 - v_projection)
if start_uv_vert == 'TOP':
if direction == POSITIVE:
new_co = (u1 + u_projection, v1 + v_projection)
if direction == NEGATIVE:
new_co = (u1 - u_projection, v1 - v_projection)
for l in next_uv_vert:
l[uv].uv = new_co
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
loops = set()
for f in bm.faces:
if f.select:
for l in f.loops:
if l[uv].select:
loops.add(l)
uv_edge_loops = get_sorted_uv_edge_loops(uv, loops)
if not uv_edge_loops:
continue
for uv_edge_loop in uv_edge_loops:
first_uv_vert = uv_edge_loop[0]
if not first_uv_vert:
continue
end_uv_vert = uv_edge_loop[-1]
first_uv_vert_co = first_uv_vert[0][uv].uv
end_uv_vert_co = end_uv_vert[0][uv].uv
u1, v1 = first_uv_vert_co[0], first_uv_vert_co[1]
u2, v2 = end_uv_vert_co[0], end_uv_vert_co[1]
slope = calc_slope(context, first_uv_vert_co, end_uv_vert_co)
number_uv_edges = len(uv_edge_loop) - 1
if self.properties.preserve_edge_length:
self.distribute_preserve_edges_length(
uv, first_uv_vert, end_uv_vert,
first_uv_vert_co, end_uv_vert_co,
u1, v1, u2, v2, slope, number_uv_edges,
uv_edge_loop)
else:
self.distribute(
uv, first_uv_vert, end_uv_vert,
first_uv_vert_co, end_uv_vert_co,
u1, v1, u2, v2, slope, number_uv_edges,
uv_edge_loop)
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,16 @@
import bpy
from bpy.types import Operator
from bpy.props import StringProperty
class ExecuteCustomOp(Operator):
bl_idname = "uv.toolkit_execute_custom_op"
bl_label = "Execute Custom Operator"
bl_description = ""
bl_options = {'INTERNAL', 'REGISTER', 'UNDO'}
exec_op: StringProperty(options={'HIDDEN'})
def execute(self, context):
exec(self.exec_op)
return {'FINISHED'}
@@ -0,0 +1,34 @@
import os
from bpy.types import Operator
from bpy.props import StringProperty
from bpy_extras.io_utils import ExportHelper
class ExportSettings(Operator, ExportHelper):
bl_idname = "uv.toolkit_export_settings"
bl_label = "Export Settings"
bl_description = "Export addon settings"
filename_ext = ".ini"
filter_glob: StringProperty(
default="*.ini",
options={'HIDDEN'},
maxlen=2000,
)
def get_addon_properties(self):
addon_prefs_path = os.path.split(__file__)[0][:-9] + "addon_preferences.py"
with open(addon_prefs_path, 'r', encoding='utf-8') as file:
for line in file:
if ": EnumProperty" in line or ": StringProperty" in line:
yield line.split(" ")[4][:-1]
def execute(self, context):
preferences = context.preferences
with open(self.filepath, 'w', encoding='utf-8') as file:
for addon_property in self.get_addon_properties():
value = getattr(preferences.addons[__name__.partition('.')[0]].preferences, addon_property)
file.write(f"{addon_property}='{value}'" + '\n')
return {'FINISHED'}
@@ -0,0 +1,63 @@
import bpy
import bmesh
from bpy_extras import bmesh_utils
from bpy.props import IntProperty, BoolProperty
from bpy.types import Operator
class FindShatteredIslands(Operator):
bl_idname = "uv.toolkit_find_shattered_islands"
bl_label = "Find Shattered Islands"
bl_description = "Find islands that contain an unusually small number of faces\n(and optionally pin them in the top-left corner)"
bl_options = {'REGISTER', 'UNDO'}
shattered_threshold: IntProperty(
name="Threshold",
description="The maximum number of faces for islands to be shattered",
min=1,
default=3
)
pin: BoolProperty(
name="Pin to Corner",
description="Move and pin islands to the top-left corner within UV space\n(handy for testing your workflow under ideal conditions)",
default=False,
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
use_uv_select_sync = scene.tool_settings.use_uv_select_sync
if use_uv_select_sync:
self.report({"INFO"}, "UV Sync enabled, selecting faces in the 3D view")
for ob in bpy.context.objects_in_mode_unique_data:
bm = bmesh.from_edit_mesh(ob.data)
uv_layer = bm.loops.layers.uv.verify()
islands = bmesh_utils.bmesh_linked_uv_islands(bm, uv_layer)
shattered_islands = [i for i in islands if len(i) <= self.shattered_threshold]
if len(shattered_islands) < 1:
continue
bm.faces.ensure_lookup_table()
for island in shattered_islands:
for face in island:
if use_uv_select_sync:
face.select = True
for loop in face.loops:
if not use_uv_select_sync:
loop[uv_layer].select = True
if self.pin:
loop[uv_layer].uv = (0, 1)
loop[uv_layer].pin_uv = True
bmesh.update_edit_mesh(ob.data)
bm.free()
return {'FINISHED'}
@@ -0,0 +1,89 @@
import bmesh
from bpy.types import Operator
from ..utils.uv_utils import (
get_bbox,
get_udim_co,
get_objects_seams,
get_islands,
deselect_all_loops_uv,
select_all_faces,
)
class FindUdimCrossing(Operator):
bl_idname = "uv.toolkit_find_udim_crossing"
bl_label = "Find UDIM crossing"
bl_description = "Find islands that cross the borders of UDIM"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
tool_settings = context.tool_settings
uv_sync_status = tool_settings.use_uv_select_sync
current_uv_select_mode = scene.tool_settings.uv_select_mode
if uv_sync_status:
tool_settings.use_uv_select_sync = False
islands_count = 0
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
deselect_all_loops_uv(uv, bm)
if uv_sync_status:
select_all_faces(bm)
for island in get_islands(uv, bm, seams):
bbox = get_bbox(uv, island)
bbox_u1, bbox_v1 = bbox[0][0], bbox[0][1]
bbox_u2, bbox_v2 = bbox[1][0], bbox[1][1]
for f in island:
for l in f.loops:
uv_vert_co = l[uv].uv
break
udim_co = get_udim_co(uv_vert_co)
udim_u1, udim_v1 = udim_co[0][0], udim_co[0][1]
udim_u2, udim_v2 = udim_co[1][0], udim_co[1][1]
if bbox_u1 < udim_u1 or bbox_v1 < udim_v1 \
or bbox_u2 > udim_u2 or bbox_v2 > udim_v2:
for f in island:
for l in f.loops:
l[uv].select = True
islands_count += 1
bmesh.update_edit_mesh(me)
if islands_count:
scene.tool_settings.uv_select_mode = 'VERTEX'
if islands_count == 1:
island = "Island"
else:
island = "Islands"
message = f"{str(islands_count)} {island} found"
self.report({'WARNING'}, message)
scene.tool_settings.uv_select_mode = current_uv_select_mode
return {'FINISHED'}
@@ -0,0 +1,73 @@
import numpy as np
import bmesh
from bpy.types import Operator
from bpy.props import FloatProperty
from ..utils.uv_utils import (
get_bbox,
calc_bbox_center,
get_bbox_size,
translate_matrix,
scale_matrix,
get_objects_seams,
get_islands,
)
class FitToBounds(Operator):
bl_idname = "uv.toolkit_fit_to_bounds"
bl_label = "Fit to Bounds"
bl_description = "Scale the island to the size of the UV space"
bl_options = {'REGISTER', 'UNDO'}
margin: FloatProperty(
name="Margin",
default=0,
step=0.1,
precision=3,
min=0,
max=0.5,
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
margin = self.margin * 2
pivot = 0.5, 0.5
pivot_u, pivot_v = pivot
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
bbox = get_bbox(uv, island)
bbox_center_u, bbox_center_v = calc_bbox_center(bbox)
bbox_width, bbox_height = get_bbox_size(bbox)
if bbox_height > bbox_width:
factor_u = factor_v = (1 - margin) / bbox_height
else:
factor_u = factor_v = (1 - margin) / bbox_width
scale = scale_matrix((factor_u, factor_v), pivot)
tranlate = translate_matrix(pivot_u - bbox_center_u, pivot_v - bbox_center_v)
convolution = np.dot(scale, tranlate)
for f in island:
for l in f.loops:
u, v = l[uv].uv
l[uv].uv = convolution.dot(np.array([u, v, 1]))[:2]
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,46 @@
import bpy
class Hotkeys(bpy.types.Operator):
bl_idname = "uv.toolkit_hotkeys"
bl_label = "Hotkeys"
bl_description = "List of hotkeys"
bl_options = {'REGISTER'}
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=330)
def draw(self, context):
layout = self.layout
layout = layout.box()
col = layout.column()
row = col.row(align=True)
row.label(text="UV Editor Pie")
row.label(text="Shift+F")
row = col.row(align=True)
row.label(text="3D View Pie")
row.label(text="Shift+F")
row = col.row(align=True)
row.label(text="Unwrap the selected faces")
row.label(text="Shift+E")
row = col.row(align=True)
row.label(text="Straighten UVs")
row.label(text="Shift+Q")
row = col.row(align=True)
row.label(text="Distribute")
row.label(text="D")
row = col.row(align=True)
row.label(text="Center Cursor and Frame All")
row.label(text="Shift+C")
row = col.row(align=True)
row.label(text="Move Island")
row.label(text="F")
row = col.row(align=True)
row.label(text="Invert Selection")
row.label(text="Ctrl+Shift+I")
row = col.row(align=True)
row.label(text="Scale Individual Origins")
row.label(text="Alt+S")
@@ -0,0 +1,30 @@
from bpy.types import Operator
from bpy.props import StringProperty
from bpy_extras.io_utils import ImportHelper
from ..functions import get_addon_preferences
class ImportSettings(Operator, ImportHelper):
bl_idname = "uv.toolkit_import_settings"
bl_label = "Import Settings"
bl_description = "Import addon settings"
filename_ext = ".ini"
filter_glob: StringProperty(
default="*.ini",
options={'HIDDEN'},
maxlen=2000,
)
def execute(self, context):
with open(self.filepath, 'r', encoding='utf-8') as addon_settings:
for item in addon_settings:
idx = item.find('=')
prop = item[:idx]
value = item[idx + 2:-2]
addon_prefs = get_addon_preferences()
setattr(addon_prefs, prop, value)
return {'FINISHED'}
@@ -0,0 +1,76 @@
import bpy
import bmesh
from bpy.props import BoolProperty
from bpy.types import Operator
from ..utils.uv_utils import (
create_list_of_loops_from_uv_selection
)
class InvertSelection(Operator):
bl_idname = "uv.toolkit_invert_selection"
bl_label = "Invert selection"
bl_description = "Invert selection locally or globally"
bl_options = {'UNDO', 'REGISTER'}
local: BoolProperty(
name="Local",
default=True,
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
current_uv_select_mode = scene.tool_settings.uv_select_mode
if self.local:
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
view_layer = context.view_layer
act_ob = view_layer.objects.active
selected_ob = tuple(context.objects_in_mode_unique_data)
bpy.ops.object.mode_set(mode='OBJECT')
for ob in selected_ob:
ob.select_set(False)
for ob in selected_ob:
view_layer.objects.active = ob
ob.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
loops = create_list_of_loops_from_uv_selection(uv, bm.faces)
bpy.ops.uv.select_linked()
for l in loops:
l[uv].select = False
bpy.ops.object.mode_set(mode='OBJECT')
ob.select_set(False)
for ob in selected_ob:
ob.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
view_layer.objects.active = act_ob
else:
bpy.ops.uv.select_all(action='INVERT')
scene.tool_settings.uv_select_mode = 'VERTEX'
scene.tool_settings.uv_select_mode = current_uv_select_mode
return {'FINISHED'}
@@ -0,0 +1,178 @@
# from time import time
from math import sqrt
from collections import defaultdict
import bmesh
from bpy.types import Operator
from ..utils.uv_utils import (
get_objects_seams,
get_islands,
get_bbox,
calc_bbox_center,
collect_island_params,
)
class MatchIslands(Operator):
bl_idname = "uv.toolkit_match_islands"
bl_label = "Match Islands"
bl_description = "Make near-identical islands identical"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def build_map(self, target_bbox, target_bbox_center, target_coords):
if len(target_coords) < 1000:
subdivisions = 2
if len(target_coords) > 1000:
subdivisions = 4
if len(target_coords) > 4000:
subdivisions = 8
if len(target_coords) > 20000:
subdivisions = 12
areas = defaultdict(list)
bbox_width = target_bbox[1][0] - target_bbox[0][0]
bbox_height = target_bbox[1][1] - target_bbox[0][1]
width = bbox_width / subdivisions
height = bbox_height / subdivisions
start_point_u = target_bbox[0][0]
start_point_v = target_bbox[0][1]
next_point_u = start_point_u
for _ in range(subdivisions):
start_point_v = start_point_v + height
for _ in range(subdivisions):
next_point_u = next_point_u + width
area_center = (next_point_u - width / 2,
start_point_v - height / 2)
u1, v1 = next_point_u, start_point_v
u2, v2 = area_center
length = sqrt((u1 - u2) * (u1 - u2) + (v1 - v2) * (v1 - v2))
min_length = length + (length / 100) * 5
u1, v1 = area_center
for point in target_coords:
u2, v2 = point
length = sqrt((u1 - u2) * (u1 - u2) + (v1 - v2) * (v1 - v2))
if length < min_length:
areas[area_center].append(point)
if not areas[area_center]:
del areas[area_center]
next_point_u = start_point_u
return areas
def find_closest_point(self, areas, current_point, target_coords):
min_length = 1000
min_length_area = 1000
area_coords = None
u1, v1 = current_point
for area in areas:
u2, v2 = area
length = sqrt((u1 - u2) * (u1 - u2) + (v1 - v2) * (v1 - v2))
if length < min_length_area:
min_length_area = length
area_coords = areas[area]
target_point = 0, 0
for point in area_coords:
u2, v2 = point
length = sqrt((u1 - u2) * (u1 - u2) + (v1 - v2) * (v1 - v2))
if length < min_length:
min_length = length
target_point = point
return target_point
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
# print('>>>>>')
# start = time()
target_island_found = False
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for target_island in get_islands(uv, bm, seams, has_selected_faces=True):
target_island_found = True
break
if target_island_found:
break
if not target_island_found:
return {'FINISHED'}
target_stats = collect_island_params(uv, target_island)
target_coords = {l[uv].uv[:]
for f in target_island
for l in f.loops}
target_bbox = get_bbox(uv, target_island)
target_bbox_center = calc_bbox_center(target_bbox)
bbox_min_u, bbox_min_v = target_bbox[0]
bbox_max_u, bbox_max_v = target_bbox[1]
tolerance = (((bbox_max_u - bbox_min_u) / 100) * 15,
((bbox_max_v - bbox_min_v) / 100) * 15)
target_bbox_center_u, target_bbox_center_v = target_bbox_center
min_tollerance_u = target_bbox_center_u - tolerance[0]
max_tollerance_u = target_bbox_center_u + tolerance[0]
min_tollerance_v = target_bbox_center_v - tolerance[1]
max_tollerance_v = target_bbox_center_v + tolerance[1]
areas = self.build_map(target_bbox, target_bbox_center, target_coords)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams):
island_has_selection = False
for f in island:
for l in f.loops:
if l[uv].select:
island_has_selection = True
if island_has_selection:
break
if island_has_selection:
continue
island_stats = collect_island_params(uv, island)
if island_stats == target_stats:
bbox = get_bbox(uv, island)
bbox_center = calc_bbox_center(bbox)
bbox_center_u, bbox_center_v = bbox_center
if min_tollerance_u < bbox_center_u < max_tollerance_u \
and min_tollerance_v < bbox_center_v < max_tollerance_v:
island_uvs = defaultdict(set)
for f in island:
for l in f.loops:
coord = l[uv].uv
for l in l.vert.link_loops:
if l[uv].uv == coord:
island_uvs[coord[:]].add(l)
for uv_vert in island_uvs:
new_co = self.find_closest_point(
areas, uv_vert, target_coords)
for l in island_uvs[uv_vert]:
l[uv].uv = new_co
bmesh.update_edit_mesh(me)
# print(f"{(time() - start): .4f} sec")
return {'FINISHED'}
@@ -0,0 +1,37 @@
import bpy
from bpy.props import EnumProperty
class MirrorSeam(bpy.types.Operator):
bl_idname = "uv.toolkit_mirror_seam"
bl_label = "Mirror Seam"
bl_description = "Mirror the selected edges and mark them with seams"
bl_options = {'REGISTER', 'UNDO'}
axis: EnumProperty(
name="Axis",
items=[
("X", "X", ""),
("Y", "Y", ""),
("Z", "Z", ""),
],
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
mt_state = context.object.data.use_mirror_topology
context.object.data.use_mirror_topology = False
bpy.ops.mesh.select_mirror(axis={self.axis}, extend=True)
context.object.data.use_mirror_topology = True
bpy.ops.mesh.select_mirror(axis={self.axis}, extend=True)
bpy.ops.mesh.mark_seam(clear=False)
context.object.data.use_mirror_topology = mt_state
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, "axis", expand=True)
@@ -0,0 +1,41 @@
import bpy
from bpy.props import BoolProperty
from bpy.types import Operator
class MoveIsland(Operator):
bl_idname = "uv.toolkit_move_island"
bl_label = "Move Island"
bl_description = "Selects the island under the cursor and activates the move tool"
bl_options = {'REGISTER', 'UNDO'}
select_island: BoolProperty(
name="Select Island",
default=True,
options={'HIDDEN'}
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
tool_settings = context.tool_settings
if scene.tool_settings.use_uv_select_sync:
current_mode = tool_settings.mesh_select_mode[:]
tool_settings.mesh_select_mode = (False, False, True)
bpy.ops.uv.select_all(action='DESELECT')
bpy.ops.uv.select_linked_pick('INVOKE_DEFAULT')
bpy.ops.transform.translate('INVOKE_DEFAULT')
if not self.select_island:
bpy.ops.uv.select_linked_pick('INVOKE_DEFAULT', deselect=True)
tool_settings.mesh_select_mode = current_mode
else:
bpy.ops.uv.select_all(action='DESELECT')
bpy.ops.uv.select_linked_pick('INVOKE_DEFAULT')
bpy.ops.transform.translate('INVOKE_DEFAULT')
if not self.select_island:
bpy.ops.uv.select_linked_pick('INVOKE_DEFAULT', deselect=True)
return {'FINISHED'}
@@ -0,0 +1,17 @@
import bpy
class OpenAddonSettings(bpy.types.Operator):
bl_idname = "uv.toolkit_open_addon_settings"
bl_label = "Settings"
bl_description = "Open addon settings in new window"
bl_options = {'REGISTER'}
def execute(self, context):
bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
bpy.data.window_managers["WinMan"].addon_search = "UV Toolkit"
bpy.context.preferences.active_section = 'ADDONS'
bpy.data.window_managers["WinMan"].addon_support = {
'OFFICIAL', 'COMMUNITY'
}
return {'FINISHED'}
@@ -0,0 +1,102 @@
import numpy as np
from collections import defaultdict
import bmesh
from bpy.types import Operator
from ..utils.uv_utils import (
get_uv_edges,
get_objects_seams,
get_islands,
calc_slope,
get_bbox,
calc_bbox_center,
min_angle_to_axis,
universal_rotation_matrix,
)
class OrientIslands(Operator):
bl_idname = "uv.toolkit_orient_islands"
bl_label = "Orient Islands"
bl_description = "Rotate Islands to the most nearest axis"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def get_uv_edges_coordinates(self, uv, island):
island_loops = {l for f in island for l in f.loops}
border_verts = []
border_loops = set()
for l in island_loops:
if l.vert.is_boundary:
border_verts.append(l.vert)
else:
vert_uv = {l[uv].uv[:] for l in l.vert.link_loops}
if len(vert_uv) != 1:
border_verts.append(l.vert)
for v in border_verts:
for l in v.link_loops:
if l in island_loops:
border_loops.add(l)
uv_edges_co = get_uv_edges(uv, border_loops)
return uv_edges_co
def collect_uv_edges_angles(self, context, uv_edges_co):
uv_edges_angles = []
for uv_edge_co in uv_edges_co:
point_a, point_b = uv_edge_co
slope = calc_slope(context, point_a, point_b)
uv_edges_angles.append(slope)
return uv_edges_angles
def get_rotation_angle(self, uv_edges_angles):
corner_counter = defaultdict(int)
for angle in uv_edges_angles:
corner_counter[angle] += 1
current_angle, number_angles = corner_counter.popitem()
for angle in corner_counter:
if number_angles < corner_counter[angle]:
current_angle = angle
number_angles = corner_counter[angle]
return current_angle
def rotate_island(self, context, uv, island, rotation_angle):
bbox = get_bbox(uv, island)
pivot = calc_bbox_center(bbox)
angle = min_angle_to_axis(rotation_angle)
rotation = universal_rotation_matrix(context, angle, pivot)
for f in island:
for l in f.loops:
u, v = l[uv].uv
l[uv].uv = rotation.dot(np.array([u, v, 1]))[:2]
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
uv_edges_co = self.get_uv_edges_coordinates(uv, island)
uv_edges_angles = self.collect_uv_edges_angles(context, uv_edges_co)
rotation_angle = self.get_rotation_angle(uv_edges_angles)
self.rotate_island(context, uv, island, rotation_angle)
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,65 @@
import numpy as np
import bmesh
from bpy.types import Operator
from ..utils.uv_utils import (
calc_slope,
min_angle_to_axis,
get_bbox,
calc_bbox_center,
universal_rotation_matrix,
get_objects_seams,
get_islands,
)
class OrientToEdge(Operator):
bl_idname = "uv.toolkit_orient_to_edge"
bl_label = "Orient to Edge"
bl_description = "Rotates an island at the selected points to the nearest axis"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
selected_uv_verts_co = {l[uv].uv.copy().freeze()
for f in island
for l in f.loops
if l[uv].select}
if not len(selected_uv_verts_co) >= 2:
continue
selected_uv_verts_co = list(selected_uv_verts_co)
point_a = selected_uv_verts_co[0]
point_b = selected_uv_verts_co[-1]
bbox = get_bbox(uv, island)
bbox_center = calc_bbox_center(bbox)
slope = calc_slope(context, point_a, point_b)
angle = min_angle_to_axis(slope)
rotation = universal_rotation_matrix(context, angle, bbox_center)
for f in island:
for l in f.loops:
u, v = l[uv].uv
l[uv].uv = rotation.dot(np.array([u, v, 1]))[:2]
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,164 @@
import random
import numpy as np
from math import radians
import bmesh
from bpy.types import Operator
from bpy.props import (
BoolProperty,
FloatProperty,
IntProperty
)
from ..utils.uv_utils import (
universal_rotation_matrix,
scale_matrix,
translate_matrix,
get_objects_seams,
get_islands,
get_bbox,
calc_bbox_center,
)
class RandomizeIslands(Operator):
bl_idname = "uv.toolkit_randomize_islands"
bl_label = "Randomize Islands"
bl_description = "Randomize uv islands position, scale, or rotation"
bl_options = {'REGISTER', 'UNDO'}
seed: IntProperty(
name="Random Seed",
default=0
)
translate_limit: FloatProperty(
name="Distance limit",
default=0,
step=0.1,
precision=3,
min=0,
max=30
)
translate_u: BoolProperty(
name="X",
default=True
)
translate_v: BoolProperty(
name="Y",
default=True
)
angle_limit: FloatProperty(
name="Angle limit",
description="Set threshold for max and min angle",
default=0,
step=0.1,
precision=1,
min=0,
max=360
)
cw: BoolProperty(
name="CW",
default=True
)
ccw: BoolProperty(
name="CCW",
default=True
)
scale_limit: FloatProperty(
name="Scale limit",
description="Set the scaling limit",
default=0,
step=0.1,
precision=3,
min=0
)
scale_uniform: BoolProperty(
name="Uniform",
default=True
)
scale_u: BoolProperty(
name="X",
default=False
)
scale_u: BoolProperty(
name="X",
default=False
)
scale_v: BoolProperty(
name="Y",
default=False
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
random.seed(self.seed)
objects_seams = get_objects_seams(context)
translate_limit = self.translate_limit
angle_limit = self.angle_limit
scale_limit = self.scale_limit
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
bbox = get_bbox(uv, island)
bbox_center = calc_bbox_center(bbox)
distance_u = random.uniform(-translate_limit, translate_limit) if self.translate_u else 0
distance_v = random.uniform(-translate_limit, translate_limit) if self.translate_v else 0
translate = translate_matrix(distance_u, distance_v)
if self.cw and self.ccw:
angle = random.uniform(-angle_limit, angle_limit)
elif self.cw:
angle = random.uniform(-angle_limit, 0)
elif self.ccw:
angle = random.uniform(0, angle_limit)
else:
angle = 0
rotate = universal_rotation_matrix(context, radians(angle), bbox_center)
scale_factor = 1 + random.uniform(-scale_limit, scale_limit) if (self.scale_uniform or self.scale_u or self.scale_v) else 1
scale_u = scale_factor if self.scale_uniform or self.scale_u else 1
scale_v = scale_factor if self.scale_uniform or self.scale_v else 1
scale = scale_matrix((scale_u, scale_v), bbox_center)
rotate_scale = np.dot(rotate, scale)
convolution = np.dot(translate, rotate_scale)
for f in island:
for l in f.loops:
u, v = l[uv].uv
l[uv].uv = convolution.dot(np.array([u, v, 1]))[:2]
bmesh.update_edit_mesh(me)
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, "seed")
layout.label(text="Translate")
layout.prop(self, "translate_limit")
layout.prop(self, "translate_u")
layout.prop(self, "translate_v")
layout.label(text="Rotate")
layout.prop(self, "angle_limit")
layout.prop(self, "cw")
layout.prop(self, "ccw")
layout.label(text="Scale")
layout.prop(self, "scale_limit")
layout.prop(self, "scale_uniform")
layout.prop(self, "scale_u")
layout.prop(self, "scale_v")
@@ -0,0 +1,93 @@
import bpy
import bmesh
class RemoveAllCheckerMaterials(bpy.types.Operator):
bl_idname = "uv.toolkit_remove_all_checker_materials"
bl_label = "Remove All Checker Materials"
bl_description = "Completely remove all checker materials and textures"
bl_options = {'REGISTER', 'UNDO'}
def remove_checker_material_slot(self, ob):
for index, slot in enumerate(ob.material_slots):
if slot.material \
and slot.material.name.startswith("uv_checker_material"):
ob.active_material_index = index
break
bpy.ops.object.material_slot_remove()
def execute(self, context):
mesh_objects = [ob for ob in bpy.data.objects if ob.type == 'MESH']
objects_has_init_material = \
[ob for ob in mesh_objects if "uv_toolkit_init_material" in ob]
objects_has_checker_material = \
[ob for ob in mesh_objects if "uv_toolkit_checker_material" in ob]
objects_has_multiple_materials = []
for ob in mesh_objects:
if not "uv_toolkit_multiple_materials" in ob \
or not bpy.ops.object.mode_set.poll():
continue
objects_has_multiple_materials.append(ob)
for ob in objects_has_init_material:
init_material_name = ob["uv_toolkit_init_material"]
init_material = bpy.data.materials.get(init_material_name)
if init_material:
material_index = ob.data.polygons[0].material_index
ob.active_material_index = material_index
ob.active_material = init_material
del(ob["uv_toolkit_init_material"])
for ob in objects_has_checker_material:
del(ob["uv_toolkit_checker_material"])
if objects_has_multiple_materials:
view_layer = context.view_layer
initial_active_ob = view_layer.objects.active
if initial_active_ob is None:
view_layer.objects.active = objects_has_multiple_materials[0]
initial_mode = context.active_object.mode
bpy.ops.object.mode_set(mode='OBJECT')
initial_selection = []
for ob in context.selected_objects:
if ob.type == 'MESH':
initial_selection.append(ob)
ob.select_set(False)
for ob in objects_has_multiple_materials:
ob.select_set(True)
# Remove checker material slot
for ob in objects_has_multiple_materials:
view_layer.objects.active = ob
self.remove_checker_material_slot(ob)
bpy.ops.object.mode_set(mode='EDIT')
# Restore initial material indices
for ob in objects_has_multiple_materials:
me = ob.data
bm = bmesh.from_edit_mesh(me)
material_idx_layer = bm.faces.layers.int.get("material_idx_layer")
if material_idx_layer:
for f in bm.faces:
f.material_index = f[material_idx_layer]
bm.faces.layers.int.remove(material_idx_layer)
del(ob["uv_toolkit_multiple_materials"])
# Restore initial selected objects
bpy.ops.object.mode_set(mode='OBJECT')
for ob in objects_has_multiple_materials:
ob.select_set(False)
for ob in initial_selection:
ob.select_set(True)
view_layer.objects.active = initial_active_ob
bpy.ops.object.mode_set(mode=initial_mode)
for image in bpy.data.images:
if image.name.startswith("uv_checker_map"):
bpy.data.images.remove(image)
for mat in bpy.data.materials:
if mat.name.startswith("uv_checker_material"):
bpy.data.materials.remove(mat)
return {'FINISHED'}
@@ -0,0 +1,18 @@
from bpy.types import Operator
class RenameUvLayers(Operator):
bl_idname = "uv.toolkit_rename_uv_layers"
bl_label = "Batch Rename"
bl_description = "Batch rename UV Sets"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
for ob in context.selected_objects:
if ob.type == "MESH":
current_layer = ob.data.uv_layers.active_index
ob.data.uv_layers.active_index = scene.uv_toolkit.uv_layer_index - 1
ob.data.uv_layers.active.name = scene.uv_toolkit.uv_layer_name
ob.data.uv_layers.active_index = current_layer
return {'FINISHED'}
@@ -0,0 +1,39 @@
from math import radians
import bpy
from bpy.props import BoolProperty
from bpy.types import Operator
class RotateIslands(Operator):
bl_idname = "uv.toolkit_rotate_islands"
bl_label = "Rotate islands"
bl_description = "Rotate the selected islands"
bl_options = {'REGISTER', 'UNDO'}
cw: BoolProperty(
default=True,
options={'HIDDEN'},
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
angle = radians(scene.uv_toolkit.island_rotation_angle)
if self.cw is False:
angle = angle * -1
if scene.uv_toolkit.island_rotation_mode == "LOCAL":
space_data = context.space_data
current_pivot_point = space_data.pivot_point
space_data.pivot_point = 'INDIVIDUAL_ORIGINS'
bpy.ops.transform.rotate(value=angle, use_proportional_edit=False)
space_data.pivot_point = current_pivot_point
if scene.uv_toolkit.island_rotation_mode == "GLOBAL":
bpy.ops.transform.rotate(value=angle, use_proportional_edit=False)
return {'FINISHED'}
@@ -0,0 +1,22 @@
import bpy
class ScaleIndividualOrigins(bpy.types.Operator):
bl_idname = "uv.toolkit_scale_individual_origins"
bl_label = "Scale Individual Origins"
bl_description = "Scale individual origins"
bl_options = {'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
space_data = context.space_data
current_pivot_point = space_data.pivot_point
space_data.pivot_point = 'INDIVIDUAL_ORIGINS'
bpy.ops.transform.resize('INVOKE_DEFAULT')
space_data.pivot_point = current_pivot_point
return {'FINISHED'}
@@ -0,0 +1,32 @@
import bpy
from bpy.types import Operator
class ScaleIslands(Operator):
bl_idname = "uv.toolkit_scale_islands"
bl_label = "Scale Islands"
bl_description = "Scale the selected islands"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
space_data = context.space_data
curent_pivot = space_data.pivot_point
bpy.ops.uv.select_linked()
if scene.uv_toolkit.island_scale_mode == "LOCAL":
space_data.pivot_point = 'INDIVIDUAL_ORIGINS'
else:
space_data.pivot_point = 'CENTER'
bpy.ops.transform.resize(value=(
scene.uv_toolkit.island_scale_x,
scene.uv_toolkit.island_scale_y, 1))
space_data.pivot_point = curent_pivot
return {'FINISHED'}
@@ -0,0 +1,58 @@
import bpy
import bmesh
from bpy.types import Operator
from ..utils.uv_utils import (
get_objects_seams,
get_islands,
)
class SelectFlippedIslands(Operator):
bl_idname = "uv.toolkit_select_flipped_islands"
bl_label = "Select Flipped Islands"
bl_description = "Select Flipped Islands"
bl_options = {'UNDO', 'REGISTER'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def is_flipped_island(self, uv, island):
for f in island:
sum_c = 0
for i in range(3):
a = f.loops[i][uv].uv
b = f.loops[(i + 1) % 3][uv].uv
c = b.cross(a)
sum_c += c
if sum_c > 0:
return True
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
current_uv_select_mode = scene.tool_settings.uv_select_mode
bpy.ops.uv.select_all(action='DESELECT')
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams):
if self.is_flipped_island(uv, island):
for f in island:
for l in f.loops:
l[uv].select = True
scene.tool_settings.uv_select_mode = 'VERTEX'
scene.tool_settings.uv_select_mode = current_uv_select_mode
return {'FINISHED'}
@@ -0,0 +1,53 @@
import bmesh
from bpy.types import Operator
from ..utils.uv_utils import get_islands, get_objects_seams
class SelectIslandBorder(Operator):
bl_idname = "uv.toolkit_select_island_border"
bl_label = "Select Island Border"
bl_description = "Select Island Border"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
current_uv_select_mode = scene.tool_settings.uv_select_mode
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True):
island_loops = set()
for f in island:
for l in f.loops:
l[uv].select = False
island_loops.add(l)
for f in island:
for e in f.edges:
if e.index in seams:
for v in e.verts:
for l in v.link_loops:
if l in island_loops:
l[uv].select = True
for l in f.loops:
if l.vert.is_boundary:
for l in l.vert.link_loops:
if l in island_loops:
l[uv].select = True
bmesh.update_edit_mesh(me)
scene.tool_settings.uv_select_mode = 'VERTEX'
scene.tool_settings.uv_select_mode = current_uv_select_mode
return {'FINISHED'}
@@ -0,0 +1,103 @@
from collections import defaultdict
import bmesh
from bpy.types import Operator
from bpy.props import IntProperty
from ..utils.uv_utils import (
get_objects_seams,
get_islands,
get_bbox,
get_bbox_size,
collect_island_params,
)
class SelectSimilarIslands(Operator):
bl_idname = "uv.toolkit_select_similar_islands"
bl_label = "Select Similar Islands"
bl_description = "Select Similar Islands"
bl_options = {'REGISTER', 'UNDO'}
threshold: IntProperty(
name="Threshold",
default=10,
min=0,
max=1000,
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def is_same_bbox_size(self, target_bbox_size, current_bbox_size):
"""Compares two islands in bbox size"""
target_bbox_width = target_bbox_size[0]
target_bbox_height = target_bbox_size[1]
delta = 0.0001 * self.threshold
target_bbox_width_min = target_bbox_width - delta
target_bbox_width_max = target_bbox_width + delta
target_bbox_height_min = target_bbox_height - delta
target_bbox_height_max = target_bbox_height + delta
current_bbox_width, current_bbox_height = current_bbox_size
if target_bbox_width_min < current_bbox_width < target_bbox_width_max \
and target_bbox_height_min < current_bbox_height < target_bbox_height_max:
return True
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
current_uv_select_mode = scene.tool_settings.uv_select_mode
# Collect the selected islands params
islands_params = defaultdict(set)
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True):
current_bbox = get_bbox(uv, island)
current_bbox_size = get_bbox_size(current_bbox)
current_island_params = collect_island_params(uv, island)
islands_params[current_island_params].add((current_bbox_size))
# need to remove the same islands to get the coordinates for packaging
if len(islands_params[current_island_params]) > 1:
found_same_islands = 0
for bbox_params in islands_params[current_island_params]:
target_bbox_size = bbox_params
if self.is_same_bbox_size(target_bbox_size, current_bbox_size):
found_same_islands += 1
if found_same_islands > 1:
for _ in range(found_same_islands - 1):
islands_params[current_island_params].discard((current_bbox_size))
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
seams = objects_seams[ob]
for island in get_islands(uv, bm, seams):
current_island_params = collect_island_params(uv, island)
target_island = islands_params.get(current_island_params)
if target_island:
current_bbox = get_bbox(uv, island)
current_bbox_size = get_bbox_size(current_bbox)
for bbox_params in target_island:
target_bbox_size = bbox_params
if self.is_same_bbox_size(target_bbox_size, current_bbox_size):
for f in island:
for l in f.loops:
l[uv].select = True
bmesh.update_edit_mesh(me)
scene.tool_settings.uv_select_mode = 'VERTEX'
scene.tool_settings.uv_select_mode = current_uv_select_mode
return {'FINISHED'}
@@ -0,0 +1,67 @@
from bpy.types import Operator
from bpy.props import EnumProperty, BoolProperty
class SetActiveUvLayer(Operator):
bl_idname = "uv.toolkit_set_active_uv_layer"
bl_label = "Set Active UV layer"
bl_description = "Set active UV layer for selected objects"
bl_options = {'REGISTER', 'UNDO'}
mode: EnumProperty(
name="Mode",
items=[
("INDEX", "Index", ""),
("NAME", "Name", "")
],
)
render: BoolProperty(
name="Active Render",
default=False,
)
cloning: BoolProperty(
name="Active Cloning",
default=True,
)
def invoke(self, context, event):
scene = context.scene
if scene.uv_toolkit.uv_layer_name != "":
self.mode = "NAME"
else:
self.mode = "INDEX"
return self.execute(context)
def execute(self, context):
scene = context.scene
ob = context.active_object
scene_layer_index = scene.uv_toolkit.uv_layer_index
for ob in context.selected_objects:
if ob.type != "MESH":
continue
if self.mode == "INDEX":
if len(ob.data.uv_layers) < scene_layer_index:
continue
ob.data.uv_layers.active_index = scene_layer_index - 1
if self.render:
ob.data.uv_layers.active.active_render = True
if self.cloning:
ob.data.uv_layers.active.active_clone = True
elif self.mode == "NAME":
for uv_layer in ob.data.uv_layers:
if uv_layer.name == scene.uv_toolkit.uv_layer_name:
uv_layer.active = True
if self.render:
uv_layer.active_render = True
if self.cloning:
uv_layer.active_clone = True
break
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, "mode", expand=True)
layout.prop(self, "render")
layout.prop(self, "cloning")
@@ -0,0 +1,85 @@
from collections import defaultdict
import bpy
import bmesh
from bpy.types import Operator
from bpy.props import BoolProperty
class SharpEdgesFromUvIslands(Operator):
bl_idname = "uv.toolkit_sharp_edges_from_uv_islands"
bl_label = "Sharp Edges From UV Islands"
bl_description = "Marks UV Island boundaries with sharp edges and activates smooth shading"
bl_options = {'REGISTER', 'UNDO'}
use_existing_seams: BoolProperty(
name="Use existing seams",
default=False,
)
@classmethod
def poll(cls, context):
return context.active_object is not None
def execute(self, context):
current_mode = context.object.mode
view_layer = context.view_layer
act_ob = view_layer.objects.active
selected_ob = list(context.selected_objects)
mesh_ob = [ob for ob in context.selected_objects if ob.type == 'MESH']
initial_selection = defaultdict(set)
if not mesh_ob:
return {'FINISHED'}
for ob in selected_ob:
if ob.type != 'MESH':
ob.select_set(False)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.shade_auto_smooth(use_auto_smooth=True,
angle=3.14159)
view_layer.objects.active = mesh_ob[0]
bpy.ops.object.mode_set(mode='EDIT')
for ob in mesh_ob:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
view_layer.objects.active = ob
bpy.ops.mesh.customdata_custom_splitnormals_clear()
for f in bm.faces:
f.smooth = True # Set all faces shaded smooth
for l in f.loops:
l.edge.smooth = True # Remove all sharp edges
if l[uv].select:
initial_selection[ob].add(l.index)
bpy.ops.uv.reveal()
bpy.ops.uv.select_all(action='SELECT')
if not self.use_existing_seams:
bpy.ops.uv.seams_from_islands(mark_seams=False, mark_sharp=True)
for ob in selected_ob:
ob.select_set(True)
view_layer.objects.active = act_ob
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for f in bm.faces:
for l in f.loops:
if l.index in initial_selection[ob]:
continue
l[uv].select = False
if self.use_existing_seams:
for e in bm.edges:
if e.seam:
e.smooth = False
bpy.ops.object.mode_set(mode=current_mode)
return {'FINISHED'}
@@ -0,0 +1,23 @@
import bpy
class SplitFacesMove(bpy.types.Operator):
bl_idname = "uv.toolkit_split_faces_move"
bl_label = "Split Faces"
bl_description = "Separates the selected faces and activates the move tool"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
bpy.ops.uv.select_split()
bpy.ops.transform.translate('INVOKE_DEFAULT')
return {'FINISHED'}
@@ -0,0 +1,67 @@
import bpy
import bmesh
from bpy.types import Operator
from ..utils.uv_utils import (
get_objects_seams,
get_islands,
get_bbox,
calc_bbox_center,
)
class StackIslands(Operator):
bl_idname = "uv.toolkit_stack_islands"
bl_label = "Stack Islands"
bl_description = "Stacks the selected islands on top of each other"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
space_data = context.space_data
initial_cursor_position = tuple(space_data.cursor_location)
bpy.ops.uv.snap_cursor(target='SELECTED')
median_point = tuple(space_data.cursor_location)
space_data.cursor_location = initial_cursor_position
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
bbox = get_bbox(uv, island)
bbox_center = calc_bbox_center(bbox)
bbox_center_u = bbox_center[0]
bbox_center_v = bbox_center[1]
median_point_u = median_point[0]
median_point_v = median_point[1]
offset_u = abs(bbox_center_u - median_point_u)
offset_v = abs(bbox_center_v - median_point_v)
for f in island:
for l in f.loops:
if bbox_center_u < median_point_u:
u = l[uv].uv[0] + offset_u
else:
u = l[uv].uv[0] - offset_u
if bbox_center_v < median_point_v:
v = l[uv].uv[1] + offset_v
else:
v = l[uv].uv[1] - offset_v
l[uv].uv = (u, v)
l[uv].select = True
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,118 @@
from collections import defaultdict
import bmesh
from bpy.types import Operator
from bpy.props import IntProperty
from ..utils.uv_utils import (
get_objects_seams,
get_islands,
get_bbox,
get_bbox_size,
calc_bbox_center,
collect_island_params,
)
class StackSimilarIslands(Operator):
bl_idname = "uv.toolkit_stack_similar_islands"
bl_label = "Stack Similar Islands"
bl_description = "Stacks identical islands on top of each other"
bl_options = {'REGISTER', 'UNDO'}
threshold: IntProperty(
name="Threshold",
default=10,
min=0,
max=1000,
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def is_same_bbox_size(self, target_bbox_size, current_bbox_size):
"""Compares two islands in bbox size"""
target_bbox_width = target_bbox_size[0]
target_bbox_height = target_bbox_size[1]
delta = 0.0001 * self.threshold
target_bbox_width_min = target_bbox_width - delta
target_bbox_width_max = target_bbox_width + delta
target_bbox_height_min = target_bbox_height - delta
target_bbox_height_max = target_bbox_height + delta
current_bbox_width, current_bbox_height = current_bbox_size
if target_bbox_width_min < current_bbox_width < target_bbox_width_max \
and target_bbox_height_min < current_bbox_height < target_bbox_height_max:
return True
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
islands_params = defaultdict(set)
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
current_bbox = get_bbox(uv, island)
current_bbox_center = calc_bbox_center(current_bbox)
current_bbox_size = get_bbox_size(current_bbox)
current_island_params = collect_island_params(uv, island)
islands_params[current_island_params].add((current_bbox_size, current_bbox_center))
# need to remove the same islands to get the coordinates for packaging
if len(islands_params[current_island_params]) > 1:
number_same_islands = 0
for bbox_params in islands_params[current_island_params]:
target_bbox_size = bbox_params[0]
if self.is_same_bbox_size(target_bbox_size, current_bbox_size):
number_same_islands += 1
if number_same_islands > 1:
for _ in range(number_same_islands - 1):
islands_params[current_island_params].discard((current_bbox_size, current_bbox_center))
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
seams = objects_seams[ob]
for island in get_islands(uv, bm, seams, islands_with_hidden_faces=False):
current_island_params = collect_island_params(uv, island)
target_island = islands_params.get(current_island_params)
if target_island:
current_bbox = get_bbox(uv, island)
current_bbox_size = get_bbox_size(current_bbox)
current_bbox_center = calc_bbox_center(current_bbox)
for bbox_params in target_island:
target_bbox_size = bbox_params[0]
if self.is_same_bbox_size(target_bbox_size, current_bbox_size):
destination_point = bbox_params[1]
current_bbox_center_u = current_bbox_center[0]
current_bbox_center_v = current_bbox_center[1]
destination_point_u = destination_point[0]
destination_point_v = destination_point[1]
offset_u = abs(current_bbox_center_u - destination_point_u)
offset_v = abs(current_bbox_center_v - destination_point_v)
for f in island:
for l in f.loops:
if current_bbox_center_u < destination_point_u:
u = l[uv].uv[0] + offset_u
else:
u = l[uv].uv[0] - offset_u
if current_bbox_center_v < destination_point_v:
v = l[uv].uv[1] + offset_v
else:
v = l[uv].uv[1] - offset_v
l[uv].uv = (u, v)
l[uv].select = True
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,149 @@
import bpy
import bmesh
from bpy.types import Operator
from bpy.props import EnumProperty, BoolProperty
from ..utils.uv_utils import (
clear_all_seams,
get_objects_seams,
get_islands
)
class StraightenIsland(Operator):
bl_idname = "uv.toolkit_straighten_island"
bl_label = "Straighten Island"
bl_description = "Straightens the selected edges and relaxes faces around"
bl_options = {'REGISTER', 'UNDO'}
method: EnumProperty(
name="Method",
items=[
("ANGLE_BASED", "Angle Based", ""),
("CONFORMAL", "Conformal", ""),
]
)
pin: BoolProperty(
name="Pin",
default=False,
)
fill_holes: BoolProperty(
name="Fill Holes",
default=True,
)
correct_aspect: BoolProperty(
name="Correct Aspect",
default=False,
)
use_subsurf_data: BoolProperty(
name="Use Subdivision Surface",
default=False,
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def straighten_island(self, context, ob, seams):
scene = context.scene
current_uv_select_mode = scene.tool_settings.uv_select_mode
scene.tool_settings.uv_select_mode = 'VERTEX'
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
initial_pins = []
initial_seams = []
initial_selection = set()
for f in bm.faces:
for l in f.loops:
if l[uv].pin_uv:
initial_pins.append(l[uv])
if l.edge.seam:
initial_seams.append(l.edge)
if l[uv].select:
initial_selection.add(l)
l[uv].pin_uv = True
for island in get_islands(uv, bm, seams, has_selected_faces=True):
for f in island:
for l in f.loops:
if l[uv].select is False:
l[uv].pin_uv = False
if self.pin:
for loop_uv in initial_pins:
loop_uv.pin_uv = True
for f in bm.faces:
for l in f.loops:
l[uv].select = True
bpy.ops.uv.seams_from_islands(mark_seams=True)
bpy.ops.uv.unwrap(method=self.method,
fill_holes=self.fill_holes,
correct_aspect=self.correct_aspect,
use_subsurf_data=self.use_subsurf_data,
margin=0)
clear_all_seams(bm)
for e in initial_seams:
e.seam = True
for f in bm.faces:
for l in f.loops:
l[uv].pin_uv = False
for loop_uv in initial_pins:
loop_uv.pin_uv = True
for f in bm.faces:
for l in f.loops:
if l in initial_selection:
continue
l[uv].select = False
scene.tool_settings.uv_select_mode = current_uv_select_mode
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
objects_seams = get_objects_seams(context)
view_layer = context.view_layer
act_ob = view_layer.objects.active
selected_ob = list(context.objects_in_mode_unique_data)
bpy.ops.uv.toolkit_distribute(
preserve_edge_length=True, align_to_nearest_axis=True)
bpy.ops.object.mode_set(mode='OBJECT')
for ob in selected_ob:
ob.select_set(False)
for ob in selected_ob:
seams = objects_seams[ob]
view_layer.objects.active = ob
ob.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
self.straighten_island(context, ob, seams)
bpy.ops.object.mode_set(mode='OBJECT')
ob.select_set(False)
for ob in selected_ob:
ob.select_set(True)
view_layer.objects.active = act_ob
bpy.ops.object.mode_set(mode='EDIT')
return {'FINISHED'}
@@ -0,0 +1,518 @@
import numpy as np
from math import sqrt
import bmesh
from bpy.types import Operator
from bpy.props import BoolProperty
from ..utils.uv_utils import (
get_objects_seams,
get_islands,
calc_slope,
min_angle_to_axis,
universal_rotation_matrix,
)
TOP = 0
BOTTOM = 1
RIGHT = 2
LEFT = 3
class Straighten(Operator):
bl_idname = "uv.toolkit_straighten"
bl_label = "Straighten UVs"
bl_description = "Align the selected faces"
bl_options = {'UNDO', 'REGISTER'}
reshape_all: BoolProperty(
name="Reshape All",
default=False
)
gridify: BoolProperty(
name="Gridify",
default=False
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def straighten(self, bm, uv, seams, context):
def get_parallel_edge(edge, face):
edge_vert_0, edge_vert_1 = edge.verts
for e in face.edges:
if edge_vert_0 not in e.verts and edge_vert_1 not in e.verts:
return e
def calc_average_edges_length(faces, seams):
# Calculate factor
if len(faces) < 4:
max_faces = len(faces)
else:
max_faces = 4
sum_edges_length = 0
sum_uv_edges_length = 0
number_of_edges = 0
for idx, f in enumerate(faces, 1):
for l in f.loops:
current_loop = l
edge = l.edge
for next_loop in edge.other_vert(l.vert).link_loops:
if next_loop in f.loops:
break
u1, v1 = current_loop[uv].uv
u2, v2 = next_loop[uv].uv
uv_edge_length = sqrt((u1 - u2) * (u1 - u2) + (v1 - v2) * (v1 - v2))
sum_edges_length += edge.calc_length()
sum_uv_edges_length += uv_edge_length
number_of_edges += 1
if idx == max_faces:
break
edges = {e for f in faces for e in f.edges}
edges_length = {}
if self.gridify:
edge_length = sum_uv_edges_length / number_of_edges
for e in edges:
edges_length[e] = edge_length
else:
while edges:
init_edge = edges.pop()
# print(init_edge.index)
visited_faces = set()
edges_ring = [init_edge]
stack = [init_edge]
while stack:
edge = stack.pop()
for f in edge.link_faces:
if f in faces and f not in visited_faces:
next_edge = get_parallel_edge(edge, f)
stack.append(next_edge)
visited_faces.add(f)
edges_ring.append(next_edge)
for e in edges_ring:
edges.discard(e)
# print("------")
sum_edges_ring_length = 0
for number_of_edges, e in enumerate(edges_ring, 1):
sum_edges_ring_length += e.calc_length()
# print(e.index, e.calc_length())
avg_edges_ring_length = sum_edges_ring_length / number_of_edges
# print("number of edges", number_of_edges)
# print("sum edges - ", sum_edges_ring_length)
# print("avg length - ", avg_edges_ring_length)
if sum_edges_length > sum_uv_edges_length:
factor = sum_edges_length / sum_uv_edges_length
avg_edge_length = avg_edges_ring_length / factor
# print(1)
else:
factor = sum_uv_edges_length / sum_edges_length
avg_edge_length = avg_edges_ring_length * factor
# print(2)
for e in edges_ring:
edges_length[e] = avg_edge_length
return edges_length
def get_uv_vert(loop):
uv_co = loop[uv].uv
return [l for l in loop.vert.link_loops if l[uv].uv == uv_co]
def align_initial_face(init_face, uv_coords, edges_length, edges_direction):
l0 = init_face.loops[0]
l1 = init_face.loops[1]
l2 = init_face.loops[2]
l3 = init_face.loops[3]
epsilon = 0.00001
slope = calc_slope(context, l0[uv].uv, l1[uv].uv)
angle = min_angle_to_axis(slope)
pivot = l0[uv].uv.to_tuple(6)
rotation = universal_rotation_matrix(context, angle, pivot)
for l in init_face.loops:
uv_vert = get_uv_vert(l)
for l in uv_vert:
u, v = l[uv].uv
uv_coords[l] = rotation.dot(np.array([u, v, 1]))[:2]
l0_U, l0_V = uv_coords[l0]
l1_U, l1_V = uv_coords[l1]
l2_U, l2_V = uv_coords[l2]
l3_U, l3_V = uv_coords[l3]
uv_vert_1 = get_uv_vert(l1)
uv_vert_2 = get_uv_vert(l2)
uv_vert_3 = get_uv_vert(l3)
# print("++++++++++")
if abs(l0_V - l1_V) < epsilon:
# print("AXIS U")
if l0_V > l3_V:
edges_direction[l0.edge] = TOP
edges_direction[l2.edge] = BOTTOM
# print("l0_V > l3_V")
if l0_U > l1_U:
# print("l0_U > l1_U")
edges_direction[l1.edge] = LEFT
edges_direction[l3.edge] = RIGHT
edge_length = edges_length[l0.edge]
for l in uv_vert_1:
u, v = uv_coords[l]
uv_coords[l] = l0_U - edge_length, v
l1_U, l1_V = uv_coords[l1]
edge_length = edges_length[l1.edge]
for l in uv_vert_2:
u, v = uv_coords[l]
uv_coords[l] = l1_U, l1_V - edge_length
l2_U, l2_V = uv_coords[l2]
else:
# print("l0_U < l1_U")
edges_direction[l1.edge] = RIGHT
edges_direction[l3.edge] = LEFT
edge_length = edges_length[l0.edge]
for l in uv_vert_1:
u, v = uv_coords[l]
uv_coords[l] = l0_U + edge_length, v
l1_U, l1_V = uv_coords[l1]
edge_length = edges_length[l1.edge]
for l in uv_vert_2:
u, v = uv_coords[l]
uv_coords[l] = l1_U, l1_V - edge_length
l2_U, l2_V = uv_coords[l2]
else:
# print("l0_V < l3_V")
edges_direction[l0.edge] = BOTTOM
edges_direction[l2.edge] = TOP
if l0_U > l1_U:
# print("l0_U > l1_U")
edges_direction[l1.edge] = LEFT
edges_direction[l3.edge] = RIGHT
edge_length = edges_length[l0.edge]
for l in uv_vert_1:
u, v = uv_coords[l]
uv_coords[l] = l0_U - edge_length, v
l1_U, l1_V = uv_coords[l1]
edge_length = edges_length[l1.edge]
for l in uv_vert_2:
u, v = uv_coords[l]
uv_coords[l] = l1_U, l1_V + edge_length
l2_U, l2_V = uv_coords[l2]
else:
# print("l0_U < l1_U")
edges_direction[l1.edge] = RIGHT
edges_direction[l3.edge] = LEFT
edge_length = edges_length[l0.edge]
for l in uv_vert_1:
u, v = uv_coords[l]
uv_coords[l] = l0_U + edge_length, v
l1_U, l1_V = uv_coords[l1]
edge_length = edges_length[l1.edge]
for l in uv_vert_2:
u, v = uv_coords[l]
uv_coords[l] = l1_U, l1_V + edge_length
l2_U, l2_V = uv_coords[l2]
for l in uv_vert_3:
u, v = uv_coords[l]
uv_coords[l] = l0_U, l2_V
if abs(l0_U - l1_U) < epsilon:
# print("AXIS V")
if l0_V > l1_V:
# print("l0_V > l1_V")
edges_direction[l1.edge] = BOTTOM
edges_direction[l3.edge] = TOP
if l0_U > l3_U:
# print("l0_U > l3_U")
edges_direction[l0.edge] = RIGHT
edges_direction[l2.edge] = LEFT
edge_length = edges_length[l0.edge]
for l in uv_vert_1:
u, v = uv_coords[l]
uv_coords[l] = u, l0_V - edge_length
l1_U, l1_V = uv_coords[l1]
edge_length = edges_length[l1.edge]
for l in uv_vert_2:
u, v = uv_coords[l]
uv_coords[l] = l1_U - edge_length, l1_V
l2_U, l2_V = uv_coords[l2]
else:
# print("l0_U < l3_U")
edges_direction[l0.edge] = LEFT
edges_direction[l2.edge] = RIGHT
edge_length = edges_length[l0.edge]
for l in uv_vert_1:
u, v = uv_coords[l]
uv_coords[l] = u, l0_V - edge_length
l1_U, l1_V = uv_coords[l1]
edge_length = edges_length[l1.edge]
for l in uv_vert_2:
u, v = uv_coords[l]
uv_coords[l] = l1_U + edge_length, l1_V
l2_U, l2_V = uv_coords[l2]
else:
# print("l0_V < l1_V")
edges_direction[l1.edge] = TOP
edges_direction[l3.edge] = BOTTOM
if l0_U > l3_U:
# print("l0_U > l3_U")
edges_direction[l0.edge] = RIGHT
edges_direction[l2.edge] = LEFT
edge_length = edges_length[l0.edge]
for l in uv_vert_1:
u, v = uv_coords[l]
uv_coords[l] = u, l0_V + edge_length
l1_U, l1_V = uv_coords[l1]
edge_length = edges_length[l1.edge]
for l in uv_vert_2:
u, v = uv_coords[l]
uv_coords[l] = l1_U - edge_length, l1_V
l2_U, l2_V = uv_coords[l2]
else:
# print("l0_U < l3_U")
edges_direction[l0.edge] = LEFT
edges_direction[l2.edge] = RIGHT
edge_length = edges_length[l0.edge]
for l in uv_vert_1:
u, v = uv_coords[l]
uv_coords[l] = u, l0_V + edge_length
l1_U, l1_V = uv_coords[l1]
edge_length = edges_length[l1.edge]
for l in uv_vert_2:
u, v = uv_coords[l]
uv_coords[l] = l1_U + edge_length, l1_V
l2_U, l2_V = uv_coords[l2]
for l in uv_vert_3:
u, v = uv_coords[l]
uv_coords[l] = l2_U, l0_V
# space_data = context.space_data
# space_data.cursor_location = l0_U, l0_V
# def print_direction(direction):
# if direction == 0:
# return "TOP"
# if direction == 1:
# return "BOTTOM"
# if direction == 2:
# return "RIGHT"
# if direction == 3:
# return "LEFT"
# edge_0_direction = edges_direction[l0.edge]
# print(f"EDGE_0 - {print_direction(edge_0_direction)}")
# edge_1_direction = edges_direction[l1.edge]
# print(f"EDGE_1 - {print_direction(edge_1_direction)}")
# edge_2_direction = edges_direction[l2.edge]
# print(f"EDGE_2 - {print_direction(edge_2_direction)}")
# edge_3_direction = edges_direction[l3.edge]
# print(f"EDGE_3 - {print_direction(edge_3_direction)}")
def align_faces(init_face, faces, edges_direction, uv_coords):
def get_other_edges(init_edge, face,
vert_from_init_edge_0, vert_from_init_edge_1):
other_edges = {}
for init_vert in (vert_from_init_edge_0, vert_from_init_edge_1):
for e in face.edges:
if e == init_edge:
continue
if init_vert in e.verts:
other_edges[init_vert] = e
if vert_from_init_edge_0 not in e.verts and vert_from_init_edge_1 not in e.verts:
last_edge = e
return [other_edges, last_edge]
def face_loop_from_vert(face, vert):
for l in face.loops:
if l.vert == vert:
return l
def edge_processing(init_edge, other_edges, face,
vert_from_init_edge_0, vert_from_init_edge_1):
edge1 = other_edges[0][vert_from_init_edge_0]
edge2 = other_edges[0][vert_from_init_edge_1]
edge3 = other_edges[1]
l0 = face_loop_from_vert(face, vert_from_init_edge_0)
l1 = face_loop_from_vert(face, vert_from_init_edge_1)
l2 = face_loop_from_vert(face, edge1.other_vert(vert_from_init_edge_0))
l3 = face_loop_from_vert(face, edge2.other_vert(vert_from_init_edge_1))
uv_vert_0 = get_uv_vert(l2)
uv_vert_1 = get_uv_vert(l3)
l0_U, l0_V = uv_coords[l0]
l1_U, l1_V = uv_coords[l1]
edge_length = edges_length[edge1]
if edges_direction[init_edge] == TOP:
for l in uv_vert_0:
uv_coords[l] = l0_U, l0_V + edge_length
for l in uv_vert_1:
uv_coords[l] = l1_U, l1_V + edge_length
if l0_U > l1_U:
edges_direction[edge1] = RIGHT
edges_direction[edge2] = LEFT
edges_direction[edge3] = TOP
else:
edges_direction[edge1] = LEFT
edges_direction[edge2] = RIGHT
edges_direction[edge3] = TOP
if edges_direction[init_edge] == RIGHT:
for l in uv_vert_0:
uv_coords[l] = l0_U + edge_length, l0_V
for l in uv_vert_1:
uv_coords[l] = l1_U + edge_length, l1_V
if l0_V > l1_V:
edges_direction[edge1] = TOP
edges_direction[edge2] = BOTTOM
edges_direction[edge3] = RIGHT
else:
edges_direction[edge1] = BOTTOM
edges_direction[edge2] = TOP
edges_direction[edge3] = RIGHT
if edges_direction[init_edge] == BOTTOM:
for l in uv_vert_0:
uv_coords[l] = l0_U, l0_V - edge_length
for l in uv_vert_1:
uv_coords[l] = l1_U, l1_V - edge_length
if l0_U > l1_U:
edges_direction[edge1] = RIGHT
edges_direction[edge2] = LEFT
edges_direction[edge3] = BOTTOM
else:
edges_direction[edge1] = LEFT
edges_direction[edge2] = RIGHT
edges_direction[edge3] = BOTTOM
if edges_direction[init_edge] == LEFT:
for l in uv_vert_0:
uv_coords[l] = l0_U - edge_length, l0_V
for l in uv_vert_1:
uv_coords[l] = l1_U - edge_length, l1_V
if l0_V > l1_V:
edges_direction[edge1] = TOP
edges_direction[edge2] = BOTTOM
edges_direction[edge3] = LEFT
else:
edges_direction[edge1] = BOTTOM
edges_direction[edge2] = TOP
edges_direction[edge3] = LEFT
visited_faces = {init_face}
stack = [e for e in init_face.edges if e.index not in seams]
while stack:
current_edge = stack.pop()
for f in current_edge.link_faces:
if f not in visited_faces and f in faces:
vert_from_init_edge_0 = current_edge.verts[0]
vert_from_init_edge_1 = current_edge.verts[1]
other_edges = get_other_edges(current_edge, f,
vert_from_init_edge_0, vert_from_init_edge_1)
edge_processing(current_edge, other_edges, f,
vert_from_init_edge_0, vert_from_init_edge_1)
visited_faces.add(f)
for e in f.edges:
if e == current_edge or e.index in seams:
continue
stack.append(e)
def apply_uv(uv_coords):
for l in uv_coords:
l[uv].uv = uv_coords[l]
for island in get_islands(uv, bm, seams, has_selected_faces=True):
if self.properties.reshape_all:
island_has_an_hidden_faces = False
for f in island:
if f.select is False:
island_has_an_hidden_faces = True
break
if island_has_an_hidden_faces:
continue
faces = {f for f in island if len(f.verts) == 4}
else:
deselected_loops_uv = set()
for f in island:
for l in f.loops:
if not l[uv].select:
deselected_loops_uv.add(f)
faces = {f for f in island
if f not in deselected_loops_uv
and f.select
and len(f.verts) == 4}
if faces:
for init_face in faces:
break
uv_coords = {}
edges_direction = {}
edges_length = calc_average_edges_length(faces, seams)
align_initial_face(init_face, uv_coords, edges_length, edges_direction)
try:
align_faces(init_face, faces, edges_direction, uv_coords)
except KeyError:
pass
apply_uv(uv_coords)
def execute(self, context):
if context.scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
seams = objects_seams[ob]
self.straighten(bm, uv, seams, context)
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,22 @@
# import bmesh
from bpy.types import Operator
from ..utils.uv_utils import (
debug_time,
)
@debug_time
def test(operator, context):
print('>>>')
class TestOp(Operator):
bl_idname = "uv.toolkit_test_op"
bl_label = "Test Operator"
bl_description = ""
bl_options = {'INTERNAL', 'UNDO'}
def execute(self, context):
test(self, context)
return {'FINISHED'}
@@ -0,0 +1,20 @@
from bpy.types import Operator
class ToggleColorMode(Operator):
bl_idname = "uv.toolkit_toggle_color_mode"
bl_label = "Toggle Color Mode"
bl_description = "Show or hide texture in viewport"
def execute(self, context):
workspace = context.workspace
for area in workspace.screens[0].areas:
for space in area.spaces:
if space.type == 'VIEW_3D':
if space.shading.type != 'WIREFRAME':
if space.shading.color_type == 'TEXTURE':
space.shading.color_type = 'OBJECT'
else:
space.shading.color_type = 'TEXTURE'
return {'FINISHED'}
@@ -0,0 +1,18 @@
import bpy
from bpy.types import Operator
class ToggleGridType(Operator):
bl_idname = "uv.toolkit_toggle_grid_type"
bl_label = "Toggle Grid Type"
bl_description = "Switches the type of standard texture"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
for image in bpy.data.images:
if image.name.startswith("uv_checker_map"):
if image.generated_type == "UV_GRID":
image.generated_type = "COLOR_GRID"
else:
image.generated_type = "UV_GRID"
return{'FINISHED'}
@@ -0,0 +1,68 @@
import bpy
import bmesh
from bpy.types import Operator
class ToggleMaterial(Operator):
bl_idname = "uv.toolkit_toggle_material"
bl_label = "Toggle Material"
bl_description = "Toggles materials between initial and checker material"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if not context.selected_objects:
self.report({'WARNING'}, 'No Objects Selected')
return {'CANCELLED'}
view_layer = context.view_layer
act_ob = view_layer.objects.active
selected_objectes = [ob for ob in context.selected_objects if ob.type == 'MESH']
if selected_objectes:
view_layer.objects.active = selected_objectes[0]
initial_mode = context.active_object.mode
if initial_mode != 'EDIT':
bpy.ops.object.mode_set(mode='EDIT')
for ob in selected_objectes:
if "uv_toolkit_checker_material" in ob:
material_index = ob.data.polygons[0].material_index
ob.active_material_index = material_index
material_slot_name = ob.material_slots[material_index].name
if material_slot_name.startswith("uv_checker_material"):
if "uv_toolkit_init_material" in ob:
init_material = bpy.data.materials.get(
ob["uv_toolkit_init_material"]
)
if init_material:
ob.active_material = init_material
else:
ob.active_material = None
else:
checker_material = bpy.data.materials.get(
ob["uv_toolkit_checker_material"]
)
if checker_material:
ob.active_material = checker_material
if "uv_toolkit_multiple_materials" in ob:
me = ob.data
bm = bmesh.from_edit_mesh(me)
if ob["uv_toolkit_multiple_materials"]:
material_idx_layer = bm.faces.layers.int.get("material_idx_layer")
if material_idx_layer:
for f in bm.faces:
f.material_index = f[material_idx_layer]
ob["uv_toolkit_multiple_materials"] = 0
else:
for index, slot in enumerate(ob.material_slots):
if slot.material:
if slot.material.name.startswith("uv_checker_material"):
break
ob.active_material_index = index
for f in bm.faces:
f.material_index = index
ob["uv_toolkit_multiple_materials"] = 1
bmesh.update_edit_mesh(me)
bpy.ops.object.mode_set(mode=initial_mode)
view_layer.objects.active = act_ob
return{'FINISHED'}
@@ -0,0 +1,113 @@
import bpy
from bpy.types import Operator
from bpy.props import (
BoolProperty,
FloatProperty,
IntProperty,
EnumProperty
)
from ..utils.uv_utils import (
get_udim_co,
)
class UdimPacking(Operator):
bl_idname = "uv.toolkit_udim_packing"
bl_label = "Pack UVs"
bl_description = "Pack islands based on 2d cursor position"
bl_options = {'REGISTER', 'UNDO'}
shape_method: EnumProperty(
name="Shape Method",
items=[("CONCAVE", "Concave", "Uses exact geometry"),
("CONVEX", "Convex", "Uses convex hull"),
("AABB", "AABB", "Uses bounding boxes")
],
default="CONCAVE"
)
use_seed: BoolProperty(
name="Use Random Seed",
description="Randomize the UV layout before packing to find better solutions",
default=False,
)
seed: IntProperty(
name="Random Seed",
default=0
)
scale: BoolProperty(
name="Scale",
default=True,
)
rotate: BoolProperty(
name="Rotate",
default=True,
)
margin_method: EnumProperty(
name="Margin Method",
items=[("SCALED", "Scaled", "Use scale of existing UVs to multiply margin"),
("ADD", "Add", "Add the margin, ignoring any UV scale"),
("FRACTION", "Fraction", "Specify a precise fraction of final UV output")
],
default="SCALED"
)
margin: FloatProperty(
name="Margin",
precision=3,
default=0.001,
step=0.3,
min=0,
)
pin: BoolProperty(
name="Pin",
description="Lock Pinned Islands, Constrain islands containing any pinned UV's",
default=False,
)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
space_data = context.space_data
if self.use_seed and not scene.tool_settings.use_uv_select_sync:
bpy.ops.uv.toolkit_randomize_islands(
seed=self.seed,
translate_limit=10,
angle_limit=360 if self.rotate else 0
)
elif self.use_seed: # FIXME: Not sure if this is the way to go but it's consistent with Randomize Islands for now
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
cursor_position = tuple(space_data.cursor_location)
udim_co = get_udim_co(cursor_position)
u, v = udim_co[1][0] - 1, udim_co[1][1] - 1
bpy.ops.uv.pack_islands(
rotate=self.rotate,
scale=self.scale,
margin_method=self.margin_method,
margin=self.margin,
pin=self.pin,
shape_method=self.shape_method
)
bpy.ops.transform.translate(value=(u, v, 0))
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
row = layout.row()
row.prop(self, "shape_method", expand=True)
layout.prop(self, "use_seed")
row = layout.row()
row.enabled = self.use_seed
row.prop(self, "seed")
layout.prop(self, "scale")
layout.prop(self, "rotate")
row = layout.row()
row.prop(self, "margin_method", expand=True)
layout.prop(self, "margin")
layout.prop(self, "pin")
@@ -0,0 +1,120 @@
import bmesh
from bpy.types import Operator
from bpy.props import FloatProperty, EnumProperty
from ..utils.uv_utils import (
get_objects_seams,
get_islands,
get_bbox,
)
class UntackIslands(Operator):
bl_idname = "uv.toolkit_unstack_islands"
bl_label = "Unstack Islands"
bl_description = "Places islands in an axis with a given indent"
bl_options = {'REGISTER', 'UNDO'}
margin: FloatProperty(
name="Margin",
default=0.005,
min=0,
max=1,
step=0.01,
precision=3,
)
axis: EnumProperty(
items=[
("U", "X", "", 0),
("-U", "-X", "", 1),
("V", "Y", "", 2),
("-V", "-Y", "", 3),
],
name="Axis",
)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, "margin")
layout.prop(self, "axis", expand=True)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
is_initial_island = False
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
bbox = get_bbox(uv, island)
if self.axis == 'U':
if not is_initial_island:
is_initial_island = True
island_bound = bbox[1][0]
else:
margin = self.margin
offset = (island_bound - bbox[0][0]) + margin
for f in island:
for l in f.loops:
u = l[uv].uv[0] + offset
v = l[uv].uv[1]
l[uv].uv = (u, v)
island_width = abs(bbox[0][0] - bbox[1][0])
island_bound = island_bound + margin + island_width
if self.axis == '-U':
if not is_initial_island:
is_initial_island = True
island_bound = bbox[0][0]
else:
margin = self.margin
offset = (island_bound - bbox[1][0]) - margin
for f in island:
for l in f.loops:
u = l[uv].uv[0] + offset
v = l[uv].uv[1]
l[uv].uv = (u, v)
island_width = abs(bbox[0][0] - bbox[1][0])
island_bound = island_bound - margin - island_width
if self.axis == 'V':
if not is_initial_island:
is_initial_island = True
island_bound = bbox[1][1]
else:
margin = self.margin
offset = (island_bound - bbox[0][1]) + margin
for f in island:
for l in f.loops:
u = l[uv].uv[0]
v = l[uv].uv[1] + offset
l[uv].uv = (u, v)
island_height = abs(bbox[0][1] - bbox[1][1])
island_bound = island_bound + margin + island_height
if self.axis == '-V':
if not is_initial_island:
is_initial_island = True
island_bound = bbox[0][1]
else:
margin = self.margin
offset = (island_bound - bbox[1][1]) - margin
for f in island:
for l in f.loops:
u = l[uv].uv[0]
v = l[uv].uv[1] + offset
l[uv].uv = (u, v)
island_height = abs(bbox[0][1] - bbox[1][1])
island_bound = island_bound - margin - island_height
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,116 @@
from collections import defaultdict
import bmesh
from bpy.types import Operator
from bpy.props import IntProperty, EnumProperty
from ..utils.uv_utils import (
get_objects_seams,
get_islands,
get_bbox,
calc_bbox_center,
)
class UnstackOverlappedUvs(Operator):
bl_idname = "uv.toolkit_unstack_overlapped_uvs"
bl_label = "Unstack Overlapped UVs"
bl_description = "Transfer an overlapped UVs to another UDIM"
bl_options = {'REGISTER', 'UNDO'}
distance: IntProperty(
name="Distance",
default=1,
min=1,
)
axis: EnumProperty(
items=[
("U", "X", "", 0),
("-U", "-X", "", 1),
("V", "Y", "", 2),
("-V", "-Y", "", 3),
],
name="Axis",
)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, "distance")
layout.prop(self, "axis", expand=True)
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
# print("------------")
all_islands = defaultdict(list)
objects_seams = get_objects_seams(context)
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
for island in get_islands(uv, bm, seams, has_selected_faces=True, islands_with_hidden_faces=False):
bbox = get_bbox(uv, island)
bbox_center_u, bbox_center_v = calc_bbox_center(bbox)
bbox_center = (float(f'{bbox_center_u:.5f}'), float(f'{bbox_center_v:.5f}'))
island_loops = {ob: [f.index for f in island]}
all_islands[bbox_center].append(island_loops)
overlapped_islands = {}
for bbox_center in all_islands:
if len(all_islands[bbox_center]) == 1:
continue
if len(all_islands[bbox_center]) > 1:
overlapped_islands[bbox_center] = all_islands[bbox_center]
overlapped_islands[bbox_center].pop()
for ob in context.objects_in_mode_unique_data:
seams = objects_seams[ob]
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
bm.faces.ensure_lookup_table()
if self.axis == "U":
for bbox_center in overlapped_islands:
for ob_islands in overlapped_islands[bbox_center]:
if ob_islands.get(ob):
for idx in ob_islands[ob]:
for l in bm.faces[idx].loops:
u, v = l[uv].uv
l[uv].uv = u + self.distance, v
if self.axis == "-U":
for bbox_center in overlapped_islands:
for ob_islands in overlapped_islands[bbox_center]:
if ob_islands.get(ob):
for idx in ob_islands[ob]:
for l in bm.faces[idx].loops:
u, v = l[uv].uv
l[uv].uv = u - self.distance, v
if self.axis == "V":
for bbox_center in overlapped_islands:
for ob_islands in overlapped_islands[bbox_center]:
if ob_islands.get(ob):
for idx in ob_islands[ob]:
for l in bm.faces[idx].loops:
u, v = l[uv].uv
l[uv].uv = u, v + self.distance
if self.axis == "-V":
for bbox_center in overlapped_islands:
for ob_islands in overlapped_islands[bbox_center]:
if ob_islands.get(ob):
for idx in ob_islands[ob]:
for l in bm.faces[idx].loops:
u, v = l[uv].uv
l[uv].uv = u, v - self.distance
bmesh.update_edit_mesh(me)
return {'FINISHED'}
@@ -0,0 +1,118 @@
import bpy
import bmesh
from bpy.types import Operator
from bpy.props import EnumProperty, BoolProperty
from ..utils.uv_utils import clear_all_seams
class UnwrapSelected(Operator):
bl_idname = "uv.toolkit_unwrap_selected"
bl_label = "Unwrap Selected"
bl_description = "Unwrap the selected faces"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
method: EnumProperty(
name="Method",
items=[
("ANGLE_BASED", "Angle Based", ""),
("CONFORMAL", "Conformal", ""),
]
)
fill_holes: BoolProperty(
name="Fill Holes",
default=True,
)
correct_aspect: BoolProperty(
name="Correct Aspect",
default=False,
)
use_subsurf_data: BoolProperty(
name="Use Subdivision Surface",
default=False,
)
def unwrap_selected_uv_verts(self, bm, uv):
initial_pins = []
initial_seams = []
initial_selection = set()
for f in bm.faces:
for l in f.loops:
if l[uv].pin_uv:
initial_pins.append(l[uv])
if l.edge.seam:
initial_seams.append(l.edge)
if l[uv].select:
initial_selection.add(l)
else:
l[uv].pin_uv = True
l[uv].select = True
bpy.ops.uv.seams_from_islands(mark_seams=True)
bpy.ops.uv.unwrap(method=self.method,
fill_holes=self.fill_holes,
correct_aspect=self.correct_aspect,
use_subsurf_data=self.use_subsurf_data,
margin=0)
clear_all_seams(bm)
for e in initial_seams:
e.seam = True
for f in bm.faces:
for l in f.loops:
l[uv].pin_uv = False
for l in initial_pins:
l.pin_uv = True
for f in bm.faces:
for l in f.loops:
if l in initial_selection:
continue
l[uv].select = False
def execute(self, context):
scene = context.scene
if scene.tool_settings.use_uv_select_sync:
self.report({'INFO'}, "Need to disable UV Sync")
return {'CANCELLED'}
view_layer = context.view_layer
act_ob = view_layer.objects.active
selected_ob = tuple(context.objects_in_mode_unique_data)
bpy.ops.object.mode_set(mode='OBJECT')
for ob in selected_ob:
ob.select_set(False)
for ob in selected_ob:
view_layer.objects.active = ob
ob.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
self.unwrap_selected_uv_verts(bm, uv)
bpy.ops.object.mode_set(mode='OBJECT')
ob.select_set(False)
for ob in selected_ob:
ob.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
view_layer.objects.active = act_ob
return {'FINISHED'}
@@ -0,0 +1,112 @@
import bmesh
from bpy.types import Operator
from ..functions import get_addon_preferences
class UvSyncMode(Operator):
bl_idname = "uv.toolkit_sync_mode"
bl_label = "UV Sync mode"
bl_description = "Toggle UV Sync Mode"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_MESH'
def sync_uv_selction_mode(self, context, uv_sync_enable):
scene = context.scene
vertex = True, False, False
edge = False, True, False
face = False, False, True
if uv_sync_enable:
uv_select_mode = scene.tool_settings.uv_select_mode
tool_settings = context.tool_settings
if uv_select_mode == 'VERTEX':
tool_settings.mesh_select_mode = vertex
if uv_select_mode == 'EDGE':
tool_settings.mesh_select_mode = edge
if uv_select_mode == 'FACE':
tool_settings.mesh_select_mode = face
else:
mesh_select_mode = context.tool_settings.mesh_select_mode[:]
tool_settings = scene.tool_settings
if mesh_select_mode == vertex:
tool_settings.uv_select_mode = 'VERTEX'
if mesh_select_mode == edge:
tool_settings.uv_select_mode = 'EDGE'
if mesh_select_mode == face:
tool_settings.uv_select_mode = 'FACE'
def sync_selected_elements(self, context, uv_sync_enable):
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv_layer = bm.loops.layers.uv.verify()
if uv_sync_enable:
for face in bm.faces:
for loop in face.loops:
loop_uv = loop[uv_layer]
if not loop_uv.select:
face.select = False
for face in bm.faces:
for loop in face.loops:
loop_uv = loop[uv_layer]
if loop_uv.select:
loop.vert.select = True
for edge in bm.edges:
vert_count = 0
for vert in edge.verts:
if vert.select:
vert_count += 1
if vert_count == 2:
edge.select = True
else:
for face in bm.faces:
for loop in face.loops:
loop_uv = loop[uv_layer]
loop_uv.select = False
mesh_select_mode = context.tool_settings.mesh_select_mode[:]
if mesh_select_mode[2]: # face
for face in bm.faces:
if face.select:
for loop in face.loops:
loop_uv = loop[uv_layer]
if loop.vert.select:
loop_uv.select = True
else:
for face in bm.faces:
for loop in face.loops:
loop_uv = loop[uv_layer]
if loop.vert.select:
loop_uv.select = True
for face in bm.faces:
face.select = True
bmesh.update_edit_mesh(me)
def execute(self, context):
addon_prefs = get_addon_preferences()
tool_settings = context.tool_settings
uv_sync_enable = not tool_settings.use_uv_select_sync
tool_settings.use_uv_select_sync = uv_sync_enable
if addon_prefs.sync_uv_selction_mode == "enable":
self.sync_uv_selction_mode(context, uv_sync_enable)
if addon_prefs.sync_selection == "enable":
self.sync_selected_elements(context, uv_sync_enable)
return {'FINISHED'}