2025-07-01
This commit is contained in:
@@ -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'}
|
||||
Reference in New Issue
Block a user