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,9 @@
import os
panel_path = os.path.split(__file__)[0][:-5] + "ui/panel.py"
with open(panel_path, 'r', encoding='utf-8') as file:
for line in file:
if 'bl_idname' in line:
panel_id = line.split('"')[-2]
print(f'"{panel_id}",')
@@ -0,0 +1,80 @@
import bpy
from ..functions import get_addon_preferences
from ..register import icons_collections
# EnumProperty
# [(identifier, name, description, icon, number),..]
uv_editor_items = [
("uv.toolkit_align_uv", "Align UV", ""),
("uv.toolkit_clear_all_seams", "Clear All Seams", ""),
("uv.toolkit_distribute", "Distribute", ""),
("uv.toolkit_find_udim_crossing", "Find UDIMs crossing", ""),
("uv.toolkit_fit_to_bounds", "Fit to Bounds", ""),
("uv.toolkit_invert_selection", "Invert Selection", ""),
("uv.toolkit_match_islands", "Match Islands", ""),
("uv.toolkit_orient_islands", "Orient Islands", ""),
("uv.toolkit_orient_to_edge", "Orient to Edge", ""),
("uv.toolkit_randomize_islands", "Randomize Islands", ""),
("uv.toolkit_remove_all_checker_materials", "Remove All Checker Materials", ""),
("uv.toolkit_select_island_border", "Select Island Border", ""),
("uv.toolkit_select_similar_islands", "Select Similar Islands", ""),
("uv.toolkit_select_flipped_islands", "Select Flipped Islands", ""),
("uv.toolkit_sharp_edges_from_uv_islands", "Sharp Edges From UV Islands", ""),
("uv.toolkit_split_faces_move", "Split Faces", ""),
("uv.toolkit_stack_islands", "Stack Islands", ""),
("uv.toolkit_stack_similar_islands", "Stack Similar Islands", ""),
("uv.toolkit_straighten_island", "Straighten Island", ""),
("uv.toolkit_straighten", "Straighten UVs", ""),
("uv.toolkit_toggle_material", "Toggle Material", ""),
("uv.toolkit_toggle_color_mode", "Toggle Color Mode", ""),
("uv.toolkit_unstack_islands", "Unstack Islands", ""),
("uv.toolkit_unstack_overlapped_uvs", "Unstack Overlapped UVs", ""),
("uv.toolkit_unwrap_selected", "Unwrap Selected", ""),
("uv.toolkit_center_cursor_and_frame_all", "Center Cursor and View All", ""),
("uv.toolkit_udim_packing", "Pack UVs", ""),
("uv.toolkit_clear_all_pins", "Clear All Pins", ""),
("PIN", "Pin", ""),
("UNPIN", "Unpin", ""),
("MARK_SEAM", "Mark Seam", ""),
("CLEAR_SEAM", "Clear Seam", ""),
("CUSTOM_OP", "Custom operator", ""),
("DISABLE", "Disable", ""),
]
view3d_items = [
("uv.toolkit_clear_all_seams", "Clear All Seams", ""),
("uv.toolkit_remove_all_checker_materials", "Remove All Checker Materials", ""),
("uv.toolkit_sharp_edges_from_uv_islands", "Sharp Edges From UV Islands", ""),
("uv.toolkit_toggle_material", "Toggle Material", ""),
("uv.toolkit_toggle_color_mode", "Toggle Color Mode", ""),
("uv.toolkit_border_seam", "Border Seam", ""),
("uv.toolkit_mirror_seam", "Mirror Seam", ""),
("uv.unwrap", "Unwrap", ""),
("MARK_SEAM", "Mark Seam", ""),
("CLEAR_SEAM", "Clear Seam", ""),
("UV_MENU", "UV menu", ""),
("CUSTOM_OP", "Custom operator", ""),
("DISABLE", "Disable", ""),
]
def get_operator_name(context, custom_op):
custom_op = custom_op.split(".")
if 3 < len(custom_op):
if hasattr(bpy.ops, custom_op[2]):
op_category = getattr(bpy.ops, custom_op[2])
op_id_end_line = custom_op[3].find("(")
op_id = custom_op[3][:op_id_end_line]
op = getattr(op_category, op_id)
return op.get_rna_type().name
return "Unknown Operator"
def get_icons_set(context, pie_menu=False):
addon_prefs = get_addon_preferences()
if addon_prefs.icon_style == 'LIGHT' or pie_menu:
icons_coll = icons_collections["light"]
else:
icons_coll = icons_collections["dark"]
return icons_coll
@@ -0,0 +1,460 @@
import numpy as np
from time import time
from math import atan, cos, sin, pi
from collections import deque, defaultdict
import bpy
import bmesh
AXIS_U = 0
AXIS_V = 1
NEGATIVE = 0
POSITIVE = 1
def debug_time(func):
def wrapper(*args, **kwargs):
start = time()
result = func(*args, **kwargs)
print(f"{func.__name__.upper()}: {(time() - start): .4f} sec")
return result
return wrapper
def calc_length_sorted_uv_edges(uv, uv_edge_loop):
uv_edges_length = []
steps = len(uv_edge_loop) - 1
for idx in range(steps):
first_uv_vert_co = uv_edge_loop[idx][0][uv].uv
second_uv_vert_co = uv_edge_loop[idx + 1][0][uv].uv
edge_length = (first_uv_vert_co - second_uv_vert_co).length
uv_edges_length.append(edge_length)
return uv_edges_length
def get_uv_edges(uv, loops):
uv_edges = set()
def get_uv_edge():
for l in loops:
next_loop = l.link_loop_next
if next_loop in loops:
yield (l[uv].uv[:], next_loop[uv].uv[:])
for uv_edge in get_uv_edge():
if (uv_edge[1], uv_edge[0]) not in uv_edges:
uv_edges.add(uv_edge)
return uv_edges
def get_sorted_uv_edge_loops(uv, loops):
def collect_linked_uv_verts(uv_vert):
def get_other_uv_vert(uv_vert, uv_edge):
for v in uv_edge:
if v != uv_vert:
other_uv_vert = v
return other_uv_vert
def get_uv_edge(uv_vert):
for uv_edge in uv_edges:
if uv_vert in uv_edge:
return uv_edge
while True:
uv_edge = get_uv_edge(uv_vert)
if uv_edge:
uv_vert = get_other_uv_vert(uv_vert, uv_edge)
uv_edges.discard(uv_edge)
yield uv_vert
else:
break
def get_sorted_uv_edge_loop():
init_uv_edge = uv_edges.pop()
first_uv_vert = init_uv_edge[0]
second_uv_vert = init_uv_edge[1]
# Walk on both sides of the loop.
# <--o--o-->
sorted_uv_verts_co = deque([first_uv_vert, second_uv_vert])
for uv_vert in collect_linked_uv_verts(first_uv_vert):
sorted_uv_verts_co.appendleft(uv_vert)
for uv_vert in collect_linked_uv_verts(second_uv_vert):
sorted_uv_verts_co.append(uv_vert)
# Grab loops from uv coordinates
sorted_uv_verts = []
for uv_vert_co in sorted_uv_verts_co:
uv_vert = [l for l in loops if l[uv].uv[:] == uv_vert_co]
sorted_uv_verts.append(uv_vert)
return sorted_uv_verts
uv_edges = get_uv_edges(uv, loops)
sorted_uv_edge_loops = []
if not uv_edges:
return
while len(uv_edges) != 0:
sorted_uv_edge_loop = get_sorted_uv_edge_loop()
if sorted_uv_edge_loop:
sorted_uv_edge_loops.append(sorted_uv_edge_loop)
return sorted_uv_edge_loops
def deselect_all_loops_uv(uv, bm):
for f in bm.faces:
for l in f.loops:
l[uv].select = False
def select_all_faces(bm):
for f in bm.faces:
f.select = True
def deselect_all_faces(bm):
for f in bm.faces:
f.select = False
def create_list_of_selected_faces(bm):
return [f for f in bm.faces if f.select]
def create_list_of_loops_from_uv_selection(uv, faces):
return [l for f in faces for l in f.loops if l[uv].select]
def create_set_of_loops_from_uv_selection(uv, faces):
return {l for f in faces for l in f.loops if l[uv].select}
def verts_from_uv_selection(uv, faces):
return {l.vert for f in faces for l in f.loops if l[uv].select}
def clear_all_seams(bm):
for e in bm.edges:
e.seam = False
def store_all_seams(bm):
return [e for e in bm.edge if e.seam]
def get_bbox(uv, faces):
"""
Return lower left point and top right point.
"""
if isinstance(faces, list):
initial_point = faces[0].loops[0][uv].uv
else:
for face in faces:
break
initial_point = face.loops[0][uv].uv
u_min, v_min = initial_point
u_max, v_max = initial_point
for f in faces:
for l in f.loops:
u, v = l[uv].uv[0], l[uv].uv[1]
if u < u_min:
u_min = u
if u_max < u:
u_max = u
if v < v_min:
v_min = v
if v_max < v:
v_max = v
return (u_min, v_min), (u_max, v_max)
def get_bbox_size(bbox):
u1, v1 = bbox[0][0], bbox[0][1]
u2, v2 = bbox[1][0], bbox[1][1]
bbox_width = u2 - u1
bbox_height = v2 - v1
return bbox_width, bbox_height
def calc_bbox_center(bbox):
u1, v1 = bbox[0][0], bbox[0][1]
u2, v2 = bbox[1][0], bbox[1][1]
return ((u2 - u1) / 2 + u1, (v2 - v1) / 2 + v1)
def get_udim_co(point):
"""
Return udim coordinates by a point coordinate.
"""
point_u, point_v = point[0], point[1]
u1, v1 = point_u // 1, point_v // 1
u2, v2 = u1 + 1, v1 + 1
return (((u1, v1)), ((u2, v2)))
def calc_slope(context, point_a, point_b):
u1, v1 = point_a[0], point_a[1]
u2, v2 = point_b[0], point_b[1]
acitve_img = get_non_square_acitve_img(context)
if acitve_img:
width, height = acitve_img.size
if u1 > u2:
pivot_u = (u1 - u2) / 2
else:
pivot_u = (u2 - u1) / 2
if v1 > v2:
pivot_v = (v1 - v2) / 2
else:
pivot_v = (v2 - v1) / 2
pivot = (pivot_u, pivot_v)
if width > height:
scale = scale_matrix((width / height, 1), pivot)
else:
scale = scale_matrix((1 / (height / width), 1), pivot)
point_a = scale.dot(np.array([u1, v1, 1]))[:2]
point_b = scale.dot(np.array([u2, v2, 1]))[:2]
u1, v1 = point_a[0], point_a[1]
u2, v2 = point_b[0], point_b[1]
if u2 - u1 == 0:
return 0
else:
return atan((v2 - v1) / (u2 - u1))
def min_angle_to_axis(slope):
"""
Returns the angle to the nearest axis.
"""
if 0 < slope:
if slope < pi / 2 - slope:
angle = slope * -1
else:
angle = pi / 2 - slope
elif slope < 0:
if slope < pi / -2 - slope:
angle = (pi / -2 - slope)
else:
angle = slope * -1
else:
angle = 0
return angle
def find_nearest_axis(slope, point_a, point_b):
u1, v1 = point_a[0], point_a[1]
u2, v2 = point_b[0], point_b[1]
if slope == 0:
if v1 == v2:
axis = AXIS_U
if u1 == u2:
axis = AXIS_V
else:
if pi / 2 - abs(slope) > abs(slope):
axis = AXIS_U
else:
axis = AXIS_V
return axis
def get_direction(axis, point_a, point_b):
u1, v1 = point_a[0], point_a[1]
u2, v2 = point_b[0], point_b[1]
if axis == AXIS_U:
if u1 < u2:
direction = POSITIVE
else:
direction = NEGATIVE
if axis == AXIS_V:
if v1 < v2:
direction = POSITIVE
else:
direction = NEGATIVE
return direction
def translate_matrix(u, v):
return np.array([
[1, 0, u],
[0, 1, v],
[0, 0, 1]])
def rotation_matrix(angle, pivot):
cos_a = float(f'{cos(angle):.6f}')
sin_a = float(f'{sin(angle):.6f}')
u, v = pivot
return np.array([
[(cos_a), (sin_a * -1), (u * (1 - cos_a) + v * sin_a)],
[(sin_a), (cos_a), (v * (1 - cos_a) - u * sin_a)],
[0, 0, 1]])
def universal_rotation_matrix(context, angle, pivot):
"""
Rotate on non square active image
"""
acitve_img = get_non_square_acitve_img(context)
rotation = rotation_matrix(angle, pivot)
if acitve_img:
width, height = acitve_img.size
if width > height:
scale_1 = scale_matrix((width / height, 1), pivot)
scale_2 = scale_matrix((1 / (width / height), 1), pivot)
else:
scale_1 = scale_matrix((1 / (height / width), 1), pivot)
scale_2 = scale_matrix((height / width, 1), pivot)
rotate_scale = np.dot(rotation, scale_1)
convolution = np.dot(scale_2, rotate_scale)
return convolution
return rotation
def scale_matrix(factor, pivot):
pivot_u, pivot_v = pivot
factor_u, factor_v = factor
return np.array([
[factor_u, 0, pivot_u * (1 - factor_u)],
[0, factor_v, pivot_v * (1 - factor_v)],
[0, 0, 1]])
def get_non_square_acitve_img(context):
for area in context.screen.areas:
if area.type == 'IMAGE_EDITOR':
acitve_img = area.spaces.active.image
if acitve_img:
width, height = acitve_img.size
if height != 0:
if width / height != 1:
return acitve_img
def get_objects_seams(context):
store_initial_seams = defaultdict(list)
objects_seams = defaultdict(set)
store_initial_selection = defaultdict(set)
store_face_selection = defaultdict(set)
store_verts_selection = defaultdict(set)
store_edges_selection = defaultdict(set)
scene = context.scene
current_uv_select_mode = scene.tool_settings.uv_select_mode
scene.tool_settings.uv_select_mode = 'VERTEX'
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 e in bm.edges:
if e.seam:
store_initial_seams[ob].append(e.index)
e.seam = False
if e.select:
store_edges_selection[ob].add(e.index)
for v in bm.verts:
if v.select:
store_verts_selection[ob].add(v.index)
for f in bm.faces:
if f.select:
store_face_selection[ob].add(f.index)
for l in f.loops:
if l[uv].select:
store_initial_selection[ob].add(l.index)
l[uv].select = True
f.select = True
bpy.ops.uv.seams_from_islands(mark_seams=True)
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
for e in bm.edges:
if e.seam:
objects_seams[ob].add(e.index)
e.seam = False
for ob in context.objects_in_mode_unique_data:
me = ob.data
bm = bmesh.from_edit_mesh(me)
uv = bm.loops.layers.uv.verify()
bm.edges.ensure_lookup_table()
for edge_idx in store_initial_seams[ob]:
bm.edges[edge_idx].seam = True
for f in bm.faces:
for l in f.loops:
if l.index in store_initial_selection[ob]:
continue
l[uv].select = False
for f in bm.faces:
if f.index in store_face_selection[ob]:
continue
f.select = False
for v in bm.verts:
if v.index in store_verts_selection[ob]:
v.select = True
for e in bm.edges:
if e.index in store_edges_selection[ob]:
e.select = True
scene.tool_settings.uv_select_mode = current_uv_select_mode
return objects_seams
def get_islands(uv, bm, seams, has_selected_faces=False, islands_with_hidden_faces=True):
if has_selected_faces:
faces = {f for f in bm.faces for l in f.loops if l[uv].select}
else:
faces = set(bm.faces)
while len(faces) != 0:
init_face = faces.pop()
island = {init_face}
stack = [init_face]
while len(stack) != 0:
face = stack.pop()
for e in face.edges:
if e.index not in seams:
for f in e.link_faces:
if f != face and f not in island:
stack.append(f)
island.add(f)
for f in island:
faces.discard(f)
if islands_with_hidden_faces is False:
island_has_a_hidden_faces = False
for face in island:
if face.select is False:
island_has_a_hidden_faces = True
# print(">>>>>")
# print("has_a_hidden_faces")
break
if island_has_a_hidden_faces:
continue
yield island
def collect_island_params(uv, faces):
number_faces = len(faces)
number_loops = 0
vert_on_seams = 0
for f in faces:
for vert in f.verts:
vert_uv = {l[uv].uv[:] for l in vert.link_loops}
if len(vert_uv) != 1:
vert_on_seams += 1
for l in f.loops:
number_loops += 1
return number_faces, vert_on_seams, number_loops