expre-gen/blender/3.4/scripts/addons/magic_uv/op/texture_lock.py

547 lines
16 KiB
Python
Raw Normal View History

2025-10-17 09:47:23 +08:00
# SPDX-License-Identifier: GPL-2.0-or-later
__author__ = "Nutti <nutti.metro@gmail.com>"
__status__ = "production"
__version__ = "6.6"
__date__ = "22 Apr 2022"
import math
from math import atan2, cos, sqrt, sin, fabs
import bpy
from bpy.props import BoolProperty
import bmesh
from mathutils import Vector
from .. import common
from ..utils.bl_class_registry import BlClassRegistry
from ..utils.property_class_registry import PropertyClassRegistry
from ..utils import compatibility as compat
def _get_vco(verts_orig, loop):
"""
Get vertex original coordinate from loop
"""
for vo in verts_orig:
if vo["vidx"] == loop.vert.index and vo["moved"] is False:
return vo["vco"]
return loop.vert.co
def _get_link_loops(vert):
"""
Get loop linked to vertex
"""
link_loops = []
for f in vert.link_faces:
adj_loops = []
for loop in f.loops:
# self loop
if loop.vert == vert:
l = loop
# linked loop
else:
for e in loop.vert.link_edges:
if e.other_vert(loop.vert) == vert:
adj_loops.append(loop)
if len(adj_loops) < 2:
return None
link_loops.append({"l": l, "l0": adj_loops[0], "l1": adj_loops[1]})
return link_loops
def _get_ini_geom(link_loop, uv_layer, verts_orig, v_orig):
"""
Get initial geometory
(Get interior angle of face in vertex/UV space)
"""
u = link_loop["l"][uv_layer].uv
v0 = _get_vco(verts_orig, link_loop["l0"])
u0 = link_loop["l0"][uv_layer].uv
v1 = _get_vco(verts_orig, link_loop["l1"])
u1 = link_loop["l1"][uv_layer].uv
# get interior angle of face in vertex space
v0v1 = v1 - v0
v0v = v_orig["vco"] - v0
v1v = v_orig["vco"] - v1
theta0 = v0v1.angle(v0v)
theta1 = v0v1.angle(-v1v)
if (theta0 + theta1) > math.pi:
theta0 = v0v1.angle(-v0v)
theta1 = v0v1.angle(v1v)
# get interior angle of face in UV space
u0u1 = u1 - u0
u0u = u - u0
u1u = u - u1
phi0 = u0u1.angle(u0u)
phi1 = u0u1.angle(-u1u)
if (phi0 + phi1) > math.pi:
phi0 = u0u1.angle(-u0u)
phi1 = u0u1.angle(u1u)
# get direction of linked UV coordinate
# this will be used to judge whether angle is more or less than 180 degree
dir0 = u0u1.cross(u0u) > 0
dir1 = u0u1.cross(u1u) > 0
return {
"theta0": theta0,
"theta1": theta1,
"phi0": phi0,
"phi1": phi1,
"dir0": dir0,
"dir1": dir1}
def _get_target_uv(link_loop, uv_layer, verts_orig, v, ini_geom):
"""
Get target UV coordinate
"""
v0 = _get_vco(verts_orig, link_loop["l0"])
lo0 = link_loop["l0"]
v1 = _get_vco(verts_orig, link_loop["l1"])
lo1 = link_loop["l1"]
# get interior angle of face in vertex space
v0v1 = v1 - v0
v0v = v.co - v0
v1v = v.co - v1
theta0 = v0v1.angle(v0v)
theta1 = v0v1.angle(-v1v)
if (theta0 + theta1) > math.pi:
theta0 = v0v1.angle(-v0v)
theta1 = v0v1.angle(v1v)
# calculate target interior angle in UV space
phi0 = theta0 * ini_geom["phi0"] / ini_geom["theta0"]
phi1 = theta1 * ini_geom["phi1"] / ini_geom["theta1"]
uv0 = lo0[uv_layer].uv
uv1 = lo1[uv_layer].uv
# calculate target vertex coordinate from target interior angle
tuv0, tuv1 = _calc_tri_vert(uv0, uv1, phi0, phi1)
# target UV coordinate depends on direction, so judge using direction of
# linked UV coordinate
u0u1 = uv1 - uv0
u0u = tuv0 - uv0
u1u = tuv0 - uv1
dir0 = u0u1.cross(u0u) > 0
dir1 = u0u1.cross(u1u) > 0
if (ini_geom["dir0"] != dir0) or (ini_geom["dir1"] != dir1):
return tuv1
return tuv0
def _calc_tri_vert(v0, v1, angle0, angle1):
"""
Calculate rest coordinate from other coordinates and angle of end
"""
angle = math.pi - angle0 - angle1
alpha = atan2(v1.y - v0.y, v1.x - v0.x)
d = (v1.x - v0.x) / cos(alpha)
a = d * sin(angle0) / sin(angle)
b = d * sin(angle1) / sin(angle)
s = (a + b + d) / 2.0
if fabs(d) < 0.0000001:
xd = 0
yd = 0
else:
r = s * (s - a) * (s - b) * (s - d)
if r < 0:
xd = 0
yd = 0
else:
xd = (b * b - a * a + d * d) / (2 * d)
yd = 2 * sqrt(r) / d
x1 = xd * cos(alpha) - yd * sin(alpha) + v0.x
y1 = xd * sin(alpha) + yd * cos(alpha) + v0.y
x2 = xd * cos(alpha) + yd * sin(alpha) + v0.x
y2 = xd * sin(alpha) - yd * cos(alpha) + v0.y
return Vector((x1, y1)), Vector((x2, y2))
def _is_valid_context(context):
# only 'VIEW_3D' space is allowed to execute
if not common.is_valid_space(context, ['VIEW_3D']):
return False
objs = common.get_uv_editable_objects(context)
if not objs:
return False
# only edit mode is allowed to execute
if context.object.mode != 'EDIT':
return False
return True
@PropertyClassRegistry()
class _Properties:
idname = "texture_lock"
@classmethod
def init_props(cls, scene):
class Props():
verts_orig = {} # { Object: verts_orig }
scene.muv_props.texture_lock = Props()
def get_func(_):
return MUV_OT_TextureLock_Intr.is_running(bpy.context)
def set_func(_, __):
pass
def update_func(_, __):
bpy.ops.uv.muv_texture_lock_intr('INVOKE_REGION_WIN')
scene.muv_texture_lock_enabled = BoolProperty(
name="Texture Lock Enabled",
description="Texture Lock is enabled",
default=False
)
scene.muv_texture_lock_lock = BoolProperty(
name="Texture Lock Locked",
description="Texture Lock is locked",
default=False,
get=get_func,
set=set_func,
update=update_func
)
scene.muv_texture_lock_connect = BoolProperty(
name="Connect UV",
default=True
)
@classmethod
def del_props(cls, scene):
del scene.muv_props.texture_lock
del scene.muv_texture_lock_enabled
del scene.muv_texture_lock_lock
del scene.muv_texture_lock_connect
@BlClassRegistry()
class MUV_OT_TextureLock_Lock(bpy.types.Operator):
"""
Operation class: Lock Texture
"""
bl_idname = "uv.muv_texture_lock_lock"
bl_label = "Lock Texture"
bl_description = "Lock Texture"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
# we can not get area/space/region from console
if common.is_console_mode():
return True
return _is_valid_context(context)
@classmethod
def is_ready(cls, context):
sc = context.scene
props = sc.muv_props.texture_lock
if props.verts_orig:
return True
return False
def execute(self, context):
props = context.scene.muv_props.texture_lock
objs = common.get_uv_editable_objects(context)
props.verts_orig = {}
for obj in objs:
bm = bmesh.from_edit_mesh(obj.data)
if common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report({'WARNING'},
"Object {} must have more than one UV map"
.format(obj.name))
return {'CANCELLED'}
props.verts_orig[obj] = [
{"vidx": v.index, "vco": v.co.copy(), "moved": False}
for v in bm.verts if v.select
]
return {'FINISHED'}
@BlClassRegistry()
@compat.make_annotations
class MUV_OT_TextureLock_Unlock(bpy.types.Operator):
"""
Operation class: Unlock Texture
"""
bl_idname = "uv.muv_texture_lock_unlock"
bl_label = "Unlock Texture"
bl_description = "Unlock Texture"
bl_options = {'REGISTER', 'UNDO'}
connect = BoolProperty(
name="Connect UV",
default=True
)
@classmethod
def poll(cls, context):
# we can not get area/space/region from console
if common.is_console_mode():
return True
sc = context.scene
props = sc.muv_props.texture_lock
if not props.verts_orig:
return False
if not MUV_OT_TextureLock_Lock.is_ready(context):
return False
if not _is_valid_context(context):
return False
return True
def execute(self, context):
sc = context.scene
props = sc.muv_props.texture_lock
objs = common.get_uv_editable_objects(context)
if set(objs) != set(props.verts_orig.keys()):
self.report({'WARNING'},
"Object list does not match between Lock and Unlock")
return {'CANCELLED'}
for obj in objs:
verts_orig = props.verts_orig[obj]
bm = bmesh.from_edit_mesh(obj.data)
if common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report({'WARNING'},
"Object {} must have more than one UV map"
.format(obj.name))
return {'CANCELLED'}
uv_layer = bm.loops.layers.uv.verify()
verts = [v.index for v in bm.verts if v.select]
# move UV followed by vertex coordinate
for vidx, v_orig in zip(verts, verts_orig):
if vidx != v_orig["vidx"]:
self.report({'ERROR'}, "Internal Error")
return {"CANCELLED"}
v = bm.verts[vidx]
link_loops = _get_link_loops(v)
result = []
for ll in link_loops:
ini_geom = _get_ini_geom(ll, uv_layer, verts_orig, v_orig)
target_uv = _get_target_uv(
ll, uv_layer, verts_orig, v, ini_geom)
result.append({"l": ll["l"], "uv": target_uv})
# connect other face's UV
if self.connect:
ave = Vector((0.0, 0.0))
for r in result:
ave = ave + r["uv"]
ave = ave / len(result)
for r in result:
r["l"][uv_layer].uv = ave
else:
for r in result:
r["l"][uv_layer].uv = r["uv"]
v_orig["moved"] = True
bmesh.update_edit_mesh(obj.data)
props.verts_orig = {}
return {'FINISHED'}
@BlClassRegistry()
class MUV_OT_TextureLock_Intr(bpy.types.Operator):
"""
Operation class: Texture Lock (Interactive mode)
"""
bl_idname = "uv.muv_texture_lock_intr"
bl_label = "Texture Lock (Interactive mode)"
bl_description = "Internal operation for Texture Lock (Interactive mode)"
__timer = None
@classmethod
def poll(cls, context):
# we can not get area/space/region from console
if common.is_console_mode():
return False
return _is_valid_context(context)
@classmethod
def is_running(cls, _):
return 1 if cls.__timer else 0
@classmethod
def handle_add(cls, ops_obj, context):
if cls.__timer is None:
cls.__timer = context.window_manager.event_timer_add(
0.10, window=context.window)
context.window_manager.modal_handler_add(ops_obj)
@classmethod
def handle_remove(cls, context):
if cls.__timer is not None:
context.window_manager.event_timer_remove(cls.__timer)
cls.__timer = None
def __init__(self):
self.__intr_verts_orig = {} # { Object: verts_orig }
self.__intr_verts = {} # { Object: verts_orig }
def __sel_verts_changed(self, context):
objs = common.get_uv_editable_objects(context)
for obj in objs:
bm = bmesh.from_edit_mesh(obj.data)
if common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
if obj not in self.__intr_verts:
return True
prev = set(self.__intr_verts[obj])
now = {v.index for v in bm.verts if v.select}
if prev != now:
return True
return False
def __reinit_verts(self, context):
objs = common.get_uv_editable_objects(context)
self.__intr_verts_orig = {}
self.__intr_verts = {}
for obj in objs:
bm = bmesh.from_edit_mesh(obj.data)
if common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
self.__intr_verts_orig[obj] = [
{"vidx": v.index, "vco": v.co.copy(), "moved": False}
for v in bm.verts if v.select]
self.__intr_verts[obj] = [v.index for v in bm.verts if v.select]
def __update_uv(self, context):
"""
Update UV when vertex coordinates are changed
"""
objs = common.get_uv_editable_objects(context)
for obj in objs:
bm = bmesh.from_edit_mesh(obj.data)
if common.check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
if not bm.loops.layers.uv:
self.report({'WARNING'},
"Object {} must have more than one UV map"
.format(obj.data))
return
uv_layer = bm.loops.layers.uv.verify()
verts = [v.index for v in bm.verts if v.select]
verts_orig = self.__intr_verts_orig[obj]
for vidx, v_orig in zip(verts, verts_orig):
if vidx != v_orig["vidx"]:
self.report({'ERROR'}, "Internal Error")
return
v = bm.verts[vidx]
link_loops = _get_link_loops(v)
result = []
for ll in link_loops:
ini_geom = _get_ini_geom(ll, uv_layer, verts_orig, v_orig)
target_uv = _get_target_uv(
ll, uv_layer, verts_orig, v, ini_geom)
result.append({"l": ll["l"], "uv": target_uv})
# UV connect option is always true, because it raises
# unexpected behavior
ave = Vector((0.0, 0.0))
for r in result:
ave = ave + r["uv"]
ave = ave / len(result)
for r in result:
r["l"][uv_layer].uv = ave
v_orig["moved"] = True
bmesh.update_edit_mesh(obj.data)
common.redraw_all_areas()
self.__intr_verts_orig[obj] = [
{"vidx": v.index, "vco": v.co.copy(), "moved": False}
for v in bm.verts if v.select]
def modal(self, context, event):
if not _is_valid_context(context):
MUV_OT_TextureLock_Intr.handle_remove(context)
return {'FINISHED'}
if not MUV_OT_TextureLock_Intr.is_running(context):
return {'FINISHED'}
if context.area:
context.area.tag_redraw()
if event.type == 'TIMER':
if self.__sel_verts_changed(context):
self.__reinit_verts(context)
else:
self.__update_uv(context)
return {'PASS_THROUGH'}
def invoke(self, context, _):
if not _is_valid_context(context):
return {'CANCELLED'}
if not MUV_OT_TextureLock_Intr.is_running(context):
MUV_OT_TextureLock_Intr.handle_add(self, context)
return {'RUNNING_MODAL'}
else:
MUV_OT_TextureLock_Intr.handle_remove(context)
if context.area:
context.area.tag_redraw()
return {'FINISHED'}