BallanceBlenderHelper/bbp_ng/OP_UV_flatten_uv.py
yyc12345 04aa879c22 feat: finish i18n extract work
- masically finish u18n extract work (most fields are extracted)
2025-01-12 15:15:29 +08:00

585 lines
24 KiB
Python

import bpy, mathutils, bmesh
import typing, enum, collections
from . import UTIL_virtools_types, UTIL_functions
#region Param Struct
class FlattenMethod(enum.IntEnum):
# The legacy flatten uv mode. Only just do space convertion for each individual faces.
Raw = enum.auto()
# The floor specific flatten uv.
# This method will make sure the continuity in V axis in uv when flatten uv.
# Only support rectangle faces.
Floor = enum.auto()
# The wood specific flatten uv.
# Similar floor, but it will force all horizontal uv edge parallel with U axis.
# Not only V axis, but also U axis' continuity will been make sure.
Wood = enum.auto()
class NeighborType(enum.IntEnum):
"""
NeighborType is used by special flatten uv to describe the direction of neighbor.
Normally we find neighbor by +V, +U direction (in UV world), these neighbors are "forward" neighbors and marked as Forward.
But if we try finding neighbor by -V, -U direction, we call these neighbors are "backward" neighbors,
and marked as VerticalBackward or HorizontalBackward by its direction.
The UV of Backward neighbor need to be processed specially so we need distinguish them with Forward neighbors.
"""
# +V, +U direction neighbor.
Forward = enum.auto()
# -V direction neighbor.
VerticalBackward = enum.auto()
# -U direction neighbor.
HorizontalBackward = enum.auto()
class FlattenParam():
mReferenceEdge: int
mUseRefPoint: bool
mFlattenMethod: FlattenMethod
mScaleSize: float
mReferencePoint: int
mReferenceUV: float
def __init__(self, use_ref_point: bool, reference_edge: int, flatten_method: FlattenMethod) -> None:
self.mReferenceEdge = reference_edge
self.mUseRefPoint = use_ref_point
self.mFlattenMethod = flatten_method
def is_valid(self) -> bool:
"""Check whether flatten params is valid"""
if self.mUseRefPoint:
# ref point should be great than 1.
# because 0 and 1 is located at the same line with reference edge.
return self.mReferencePoint > 1
else:
# zero scale size make no sense.
return round(self.mScaleSize, 7) != 0.0
@classmethod
def create_by_scale_size(cls, reference_edge: int, flatten_method: FlattenMethod, scale_num: float):
val = cls(False, reference_edge, flatten_method)
val.mScaleSize = scale_num
return val
@classmethod
def create_by_ref_point(cls, reference_edge: int, flatten_method: FlattenMethod, ref_point: int, ref_point_uv: float):
val = cls(True, reference_edge, flatten_method)
val.mReferencePoint = ref_point
val.mReferenceUV = ref_point_uv
return val
#endregion
class BBP_OT_flatten_uv(bpy.types.Operator):
"""Flatten selected face UV. Only works for convex face"""
bl_idname = "bbp.flatten_uv"
bl_label = "Flatten UV"
bl_options = {'REGISTER', 'UNDO'}
bl_translation_context = 'BBP_OT_flatten_uv'
reference_edge: bpy.props.IntProperty(
name = "Reference Edge",
description = "The references edge of UV.\nIt will be placed in V axis.",
min = 0,
soft_min = 0, soft_max = 3,
default = 0,
translation_context = 'BBP_OT_flatten_uv/property'
) # type: ignore
flatten_method: bpy.props.EnumProperty(
name = "Flatten Method",
items = [
('RAW', "Raw", "Legacy flatten UV."),
('FLOOR', "Floor", "Floor specific flatten UV."),
('WOOD', "Wood", "Wood specific flatten UV."),
],
default = 'RAW',
translation_context = 'BBP_OT_flatten_uv/property'
) # type: ignore
scale_mode: bpy.props.EnumProperty(
name = "Scale Mode",
items = [
('NUM', "Scale Size", "Scale UV with specific number."),
('REF', "Ref. Point", "Scale UV with Reference Point feature."),
],
default = 'NUM',
translation_context = 'BBP_OT_flatten_uv/property'
) # type: ignore
scale_number: bpy.props.FloatProperty(
name = "Scale Size",
description = "The size which will be applied for scale.",
min = 0,
soft_min = 0, soft_max = 5,
default = 5.0,
step = 10,
precision = 1,
translation_context = 'BBP_OT_flatten_uv/property'
) # type: ignore
reference_point: bpy.props.IntProperty(
name = "Reference Point",
description = "The references point of UV.\nIt's U component will be set to the number specified by Reference Point UV.\nThis point index is related to the start point of reference edge.",
min = 2, # 0 and 1 is invalid. we can not order the reference edge to be set on the outside of uv axis
soft_min = 2, soft_max = 3,
default = 2,
translation_context = 'BBP_OT_flatten_uv/property'
) # type: ignore
reference_uv: bpy.props.FloatProperty(
name = "Reference Point UV",
description = "The U component which should be applied to references point in UV.",
soft_min = 0, soft_max = 1,
default = 0.5,
step = 10,
precision = 2,
translation_context = 'BBP_OT_flatten_uv/property'
) # type: ignore
@classmethod
def poll(cls, context):
obj = context.active_object
if obj is None:
return False
if obj.type != 'MESH':
return False
if obj.mode != 'EDIT':
return False
return True
def execute(self, context):
# construct scale data
flatten_method_: FlattenMethod
match(self.flatten_method):
case 'RAW': flatten_method_ = FlattenMethod.Raw
case 'FLOOR': flatten_method_ = FlattenMethod.Floor
case 'WOOD': flatten_method_ = FlattenMethod.Wood
case _: return {'CANCELLED'}
flatten_param_: FlattenParam
if self.scale_mode == 'NUM':
flatten_param_ = FlattenParam.create_by_scale_size(self.reference_edge, flatten_method_, self.scale_number)
else:
flatten_param_ = FlattenParam.create_by_ref_point(self.reference_edge, flatten_method_, self.reference_point, self.reference_uv)
if not flatten_param_.is_valid():
return {'CANCELLED'}
# do flatten uv and report
failed: int = _flatten_uv_wrapper(context.active_object.data, flatten_param_)
if failed != 0:
tr_text: str = bpy.app.translations.pgettext_rpt(
'[Flatten UV] {0} faces are not be processed correctly because process failed.', 'BBP_OT_flatten_uv/execute')
print(tr_text.format(failed))
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.emboss = 'NORMAL'
layout.label(text="Flatten Method", text_ctxt='BBP_OT_flatten_uv/draw')
sublayout = layout.row()
sublayout.prop(self, "flatten_method", expand = True)
layout.prop(self, "reference_edge")
layout.separator()
layout.label(text="Scale Mode", text_ctxt='BBP_OT_flatten_uv/draw')
sublayout = layout.row()
sublayout.prop(self, "scale_mode", expand = True)
layout.separator()
layout.label(text="Scale Configuration", text_ctxt='BBP_OT_flatten_uv/draw')
if self.scale_mode == 'NUM':
layout.prop(self, "scale_number")
else:
layout.prop(self, "reference_point")
layout.prop(self, "reference_uv")
#region BMesh Visitor Helper
def _set_face_vertex_uv(face: bmesh.types.BMFace, uv_layer: bmesh.types.BMLayerItem, idx: int, uv: UTIL_virtools_types.ConstVxVector2) -> None:
"""
Help function to set UV data for face.
@param face[in] The face to be set.
@param uv_layer[in] The corresponding uv layer. Hint: it was gotten from BMesh.loops.layers.uv.verify()
@param idx[in] The index of trying setting vertex.
@param uv[in] The set UV data
"""
face.loops[idx][uv_layer].uv = uv
def _get_face_vertex_uv(face: bmesh.types.BMFace, uv_layer: bmesh.types.BMLayerItem, idx: int) -> UTIL_virtools_types.ConstVxVector2:
"""
Help function to get UV data for face.
@param face[in] The face to be set.
@param uv_layer[in] The corresponding uv layer. Hint: it was gotten from BMesh.loops.layers.uv.verify()
@param idx[in] The index of trying setting vertex.
@return The UV data
"""
v: mathutils.Vector = face.loops[idx][uv_layer].uv
return (v[0], v[1])
def _get_face_vertex_pos(face: bmesh.types.BMFace, idx: int) -> UTIL_virtools_types.ConstVxVector3:
"""
Help function to get vertex position from face by provided index.
No index overflow checker. Caller must make sure the provided index is not overflow.
@param face[in] Bmesh face struct.
@param idx[in] The index of trying getting vertex.
@return The gotten vertex position.
"""
v: mathutils.Vector = face.loops[idx].vert.co
return (v[0], v[1], v[2])
def _circular_clamp_index(v: int, vmax: int) -> int:
"""
Circular clamp face vertex index.
Used by _real_flatten_uv.
@param v[in] The index to clamp
@param vmax[in] The count of used face vertex. At least 3.
@return The circular clamped value ranging from 0 to vmax.
"""
return v % vmax
#endregion
#region Real Worker Functions
def _flatten_uv_wrapper(mesh: bpy.types.Mesh, flatten_param: FlattenParam) -> int:
# create bmesh modifier
bm: bmesh.types.BMesh = bmesh.from_edit_mesh(mesh)
# use verify() to make sure there is a uv layer to write data
# verify() will return existing one or create one if no layer existing.
uv_layers: bmesh.types.BMLayerCollection = bm.loops.layers.uv
uv_layer: bmesh.types.BMLayerItem = uv_layers.verify()
# invoke core
failed: int
match(flatten_param.mFlattenMethod):
case FlattenMethod.Raw:
failed = _raw_flatten_uv(bm, uv_layer, flatten_param)
case FlattenMethod.Floor | FlattenMethod.Wood:
failed = _specific_flatten_uv(bm, uv_layer, flatten_param)
# show the updates in the viewport
bmesh.update_edit_mesh(mesh)
# return process result
return failed
def _raw_flatten_uv(bm: bmesh.types.BMesh, uv_layer: bmesh.types.BMLayerItem, flatten_param: FlattenParam) -> int:
# failed counter
failed: int = 0
# raw flatten uv always use zero offset
c_ZeroOffset: mathutils.Vector = mathutils.Vector((0, 0))
# process each face
face: bmesh.types.BMFace
for face in bm.faces:
# check requirement
# skip not selected face
if not face.select: continue
# skip the face that not fufill reference edge requirement
edge_count: int = len(face.loops)
if flatten_param.mReferenceEdge >= edge_count:
failed += 1
continue
# skip ref point overflow when using ref point mode
if flatten_param.mUseRefPoint and (flatten_param.mReferencePoint >= edge_count):
failed += 1
continue
# process this face
_flatten_face_uv(face, uv_layer, flatten_param, c_ZeroOffset)
return failed
def _specific_flatten_uv(bm: bmesh.types.BMesh, uv_layer: bmesh.types.BMLayerItem, flatten_param: FlattenParam) -> int:
# failed counter
failed: int = 0
# reset selected face's tag to False to indicate these face is not processed
face: bmesh.types.BMFace
for face in bm.faces:
if face.select:
face.tag = False
# prepare a function to check whether face is valid
def face_validator(f: bmesh.types.BMFace) -> bool:
# specify using external failed counter
nonlocal failed
# a valid face must be
# selected, not processed, and should be rectangle
# we check selection first
# then check tag. if tag == True, it mean this face has been processed.
if not f.select or f.tag: return False
# now this face can be processed, we need check whether it is rectangle
if len(f.loops) == 4:
# yes it is rectangle
return True
else:
# no, it is not rectangle
# we need mark its tag as True to prevent any possible recursive checking
# because it definately can not be processed in future.
f.tag = True
# then we report this face failed
failed = failed + 1
# return false
return False
# prepare face getter which will be used when stack is empty
face_getter: typing.Iterator[bmesh.types.BMFace] = filter(
lambda f: face_validator(f),
typing.cast(typing.Iterable[bmesh.types.BMFace], bm.faces)
)
# prepare a neighbor getter.
# this function will help finding the valid neighbor of specified face
# `loop_idx` is the index of loop getting from given face.
# `exp_loop_idx` is the expected index of neighbor loop in neighbor face.
def face_neighbor_getter(f: bmesh.types.BMFace, loop_idx: int, exp_loop_idx: int) -> bmesh.types.BMFace | None:
# get this face's loop
this_loop: bmesh.types.BMLoop = f.loops[loop_idx]
# check requirement for this loop
# this edge should be shared exactly by 2 faces.
#
# Manifold: For a mesh to be manifold, every edge must have exactly two adjacent faces.
# Ref: https://github.com/rlguy/Blender-FLIP-Fluids/wiki/Manifold-Meshes
if not this_loop.edge.is_manifold:
return None
# get neighbor loop
neighbor_loop: bmesh.types.BMLoop = this_loop.link_loop_radial_next
# get neighbor face and check it
neighbor_f: bmesh.types.BMFace = neighbor_loop.face
if not face_validator(neighbor_f):
return None
# check expected neighbor index
if neighbor_loop != neighbor_f.loops[exp_loop_idx]:
return None
# all check done, return face
return neighbor_f
# prepare face stack.
# NOTE: all face inserted into this stack should be marked as processed first.
face_stack: collections.deque[tuple[bmesh.types.BMFace, mathutils.Vector, NeighborType]] = collections.deque()
# start process faces
while True:
# if no item in face stack, pick one from face getter and mark it as processed
# if face getter failed, it mean that no more face, exit.
if len(face_stack) == 0:
try:
f = next(face_getter)
f.tag = True
face_stack.append((f, mathutils.Vector((0, 0)), NeighborType.Forward))
except StopIteration:
break
# pick one face from stack and process it
(face, face_offset, face_backward) = face_stack.pop()
_flatten_face_uv(face, uv_layer, flatten_param, face_offset)
# get 4 point uv because we need use them later
# NOTE: 4 uv point following this order
# +-----------+
# |(1) |(2)
# | |
# |(0) |(3)
# +-----------+
# So the loop index is
# (1)
# +---------->+
# ^ |
# |(0) |(2)
# | v
# +<----------+
# (3)
ind0 = _circular_clamp_index(flatten_param.mReferenceEdge, 4)
ind1 = _circular_clamp_index(flatten_param.mReferenceEdge + 1, 4)
ind2 = _circular_clamp_index(flatten_param.mReferenceEdge + 2, 4)
ind3 = _circular_clamp_index(flatten_param.mReferenceEdge + 3, 4)
uv0 = _get_face_vertex_uv(face, uv_layer, ind0)
uv1 = _get_face_vertex_uv(face, uv_layer, ind1)
uv2 = _get_face_vertex_uv(face, uv_layer, ind2)
uv3 = _get_face_vertex_uv(face, uv_layer, ind3)
# correct rectangle shape when in wood mode
if flatten_param.mFlattenMethod == FlattenMethod.Wood:
# make its uv geometry to rectangle from a trapezium.
# get the average U factor from its right edge.
# and make top + bottom uv edge be parallel with U axis by using left edge V factor.
average_u = (uv2[0] + uv3[0]) / 2
uv2 = (average_u, uv1[1])
uv3 = (average_u, uv0[1])
_set_face_vertex_uv(face, uv_layer, ind2, uv2)
_set_face_vertex_uv(face, uv_layer, ind3, uv3)
# do backward correction
# in backward mode, we can not know how many space backward one will occupied,
# thus we can not pass it by offset because we don't know the offset,
# so we only can patch it after computing its real size.
if face_backward != NeighborType.Forward:
if face_backward == NeighborType.VerticalBackward:
# in vertical backward patch,
# minus self height for all uv.
self_height: float = uv1[1] - uv0[1]
uv0 = (uv0[0], uv0[1] - self_height)
uv1 = (uv1[0], uv1[1] - self_height)
uv2 = (uv2[0], uv2[1] - self_height)
uv3 = (uv3[0], uv3[1] - self_height)
if face_backward == NeighborType.HorizontalBackward:
# in horizontal backward patch, minus self width for all uv.
# because we have process rectangle shape issue before this,
# so we can pick uv2 or uv3 to get width directly.
self_width: float = uv3[0] - uv0[0]
uv0 = (uv0[0] - self_width, uv0[1])
uv1 = (uv1[0] - self_width, uv1[1])
uv2 = (uv2[0] - self_width, uv2[1])
uv3 = (uv3[0] - self_width, uv3[1])
# set modified uv to geometry
_set_face_vertex_uv(face, uv_layer, ind0, uv0)
_set_face_vertex_uv(face, uv_layer, ind1, uv1)
_set_face_vertex_uv(face, uv_layer, ind2, uv2)
_set_face_vertex_uv(face, uv_layer, ind3, uv3)
# insert horizontal neighbor only in wood mode.
if flatten_param.mFlattenMethod == FlattenMethod.Wood:
# insert right neighbor (forward)
r_face: bmesh.types.BMFace | None = face_neighbor_getter(face, ind2, ind0)
if r_face is not None:
# mark it as processed
r_face.tag = True
# insert face with extra horizontal offset.
face_stack.append((r_face, mathutils.Vector((uv3[0], uv3[1])), NeighborType.Forward))
# insert left neighbor (backward)
# swap the index param of neighbor getter
l_face: bmesh.types.BMFace | None = face_neighbor_getter(face, ind0, ind2)
if l_face is not None:
l_face.tag = True
# pass origin pos, and order backward correction
face_stack.append((l_face, mathutils.Vector((uv0[0], uv0[1])), NeighborType.HorizontalBackward))
# insert vertical neighbor
# insert top neighbor (forward)
t_face: bmesh.types.BMFace | None = face_neighbor_getter(face, ind1, ind3)
if t_face is not None:
# mark it as processed
t_face.tag = True
# insert face with extra vertical offset.
face_stack.append((t_face, mathutils.Vector((uv1[0], uv1[1])), NeighborType.Forward))
# insert bottom neighbor (backward)
# swap the index param of neighbor getter
b_face: bmesh.types.BMFace | None = face_neighbor_getter(face, ind3, ind1)
if b_face is not None:
b_face.tag = True
# pass origin pos, and order backward correction
face_stack.append((b_face, mathutils.Vector((uv0[0], uv0[1])), NeighborType.VerticalBackward))
return failed
def _flatten_face_uv(face: bmesh.types.BMFace, uv_layer: bmesh.types.BMLayerItem, flatten_param: FlattenParam, offset: mathutils.Vector) -> None:
# ========== get correct new corrdinate system ==========
# yyc mark:
# we use 3 points located in this face to calc
# the base of this local uv corredinate system.
# however if this 3 points are set in a line,
# this method will cause a error, zero vector error.
#
# if z axis is zero vector, we will try using face normal instead
# to try getting correct data.
#
# zero base is not important. because it will not raise any math exception
# just a weird uv. user will notice this problem.
# get point
all_point: int = len(face.loops)
pidx_start: int = _circular_clamp_index(flatten_param.mReferenceEdge, all_point)
p1: mathutils.Vector = mathutils.Vector(_get_face_vertex_pos(face, pidx_start))
p2: mathutils.Vector = mathutils.Vector(_get_face_vertex_pos(face, _circular_clamp_index(flatten_param.mReferenceEdge + 1, all_point)))
p3: mathutils.Vector = mathutils.Vector(_get_face_vertex_pos(face, _circular_clamp_index(flatten_param.mReferenceEdge + 2, all_point)))
# get y axis
new_y_axis: mathutils.Vector = p2 - p1
new_y_axis.normalize()
vec1: mathutils.Vector = p3 - p2
vec1.normalize()
# get z axis
new_z_axis: mathutils.Vector = vec1.cross(new_y_axis)
new_z_axis.normalize()
# if z is a zero vector, use face normal instead
# please note we need use inverted face normal.
if not any(round(v, 7) for v in new_z_axis):
new_z_axis = typing.cast(mathutils.Vector, face.normal).normalized()
new_z_axis.negate()
# get x axis
new_x_axis: mathutils.Vector = new_y_axis.cross(new_z_axis)
new_x_axis.normalize()
# construct rebase matrix
origin_base: mathutils.Matrix = mathutils.Matrix((
(1.0, 0, 0),
(0, 1.0, 0),
(0, 0, 1.0)
))
origin_base.invert_safe()
new_base: mathutils.Matrix = mathutils.Matrix((
(new_x_axis.x, new_y_axis.x, new_z_axis.x),
(new_x_axis.y, new_y_axis.y, new_z_axis.y),
(new_x_axis.z, new_y_axis.z, new_z_axis.z)
))
transition_matrix: mathutils.Matrix = typing.cast(mathutils.Matrix, origin_base @ new_base)
transition_matrix.invert_safe()
# ===== rescale correction =====
rescale: float = 0.0
if flatten_param.mUseRefPoint:
# ref point method
# get reference point from loop
pidx_refp: int = _circular_clamp_index(pidx_start + flatten_param.mReferencePoint, all_point)
pref: mathutils.Vector = mathutils.Vector(_get_face_vertex_pos(face, pidx_refp)) - p1
# calc its U component
vec_u: float = abs(typing.cast(mathutils.Vector, transition_matrix @ pref).x)
if round(vec_u, 7) == 0.0:
rescale = 1.0 # fallback. rescale = 1 will not affect anything
else:
rescale = flatten_param.mReferenceUV / vec_u
else:
# scale size method
# apply rescale directly
rescale = 1.0 / flatten_param.mScaleSize
# construct matrix
# we only rescale U component (X component)
# and constant 5.0 scale for V component (Y component)
scale_matrix: mathutils.Matrix = mathutils.Matrix((
(rescale, 0, 0),
(0, 1.0 / 5.0, 0),
(0, 0, 1.0)
))
# order can not be changed. we order do transition first, then scale it.
rescale_transition_matrix: mathutils.Matrix = typing.cast(mathutils.Matrix, scale_matrix @ transition_matrix)
# ========== process each face ==========
for idx in range(all_point):
# compute uv
pp: mathutils.Vector = mathutils.Vector(_get_face_vertex_pos(face, idx)) - p1
ppuv: mathutils.Vector = typing.cast(mathutils.Vector, rescale_transition_matrix @ pp)
# u and v component has been calculated properly. no extra process needed.
# just get abs for the u component
ppuv.x = abs(ppuv.x)
# add offset and assign to uv
_set_face_vertex_uv(face, uv_layer, idx, (ppuv.x + offset.x, ppuv.y + offset.y))
#endregion
def register() -> None:
bpy.utils.register_class(BBP_OT_flatten_uv)
def unregister() -> None:
bpy.utils.unregister_class(BBP_OT_flatten_uv)