Documente Academic
Documente Profesional
Documente Cultură
#
#
#
#
#
#
#
#
#
#
#
#
#
bl_info = {
"name": "UV Squares",
"description": "UV Editor tool for reshaping selection to grid.",
"author": "Reslav Hollos",
"version": (1, 4, 21),
"blender": (2, 71, 0),
"category": "Mesh",
#"location": "UV Image Editor > UVs > UVs to grid of squares",
#"warning": "",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/UV/
Uv_Squares"
}
import bpy
import bmesh
from collections import defaultdict
from math import radians, hypot
import time
precision = 3
#todo:
#todo:
#todo:
#todo:
make joining radius scale with editor zoom rate or average unit length
align to axis by respect to vert distance
snap 2dCursor to closest selected vert (when more vertices are selected
rip different vertex on each press
if len(filteredVerts) is 0: return
if len(filteredVerts) is 1:
SnapCursorToClosestSelected(filteredVerts)
return
cursorClosestTo = CursorClosestTo(filteredVerts)
#line is selected
if len(selFaces) is 0:
if snapToClosest is True:
SnapCursorToClosestSelected(filteredVerts)
return
VertsDictForLine(uv_layer, bm, filteredVerts, vertsDict)
if AreVectsLinedOnAxis(filteredVerts) is False:
ScaleTo0OnAxisAndCursor(filteredVerts, vertsDict, cursorClosestTo)
return SuccessFinished(me, startTime)
MakeEqualDistanceBetweenVertsInLine(filteredVerts, vertsDict, cursorClos
estTo)
return SuccessFinished(me, startTime)
#else:
#active face checks
if targetFace is None or targetFace.select is False or len(targetFace.verts)
is not 4:
targetFace = selFaces[0]
else:
for l in targetFace.loops:
if l[uv_layer].select is False:
targetFace = selFaces[0]
break
ShapeFace(uv_layer, operator, targetFace, vertsDict, square)
for nf in nonQuadFaces:
for l in nf.loops:
luv = l[uv_layer]
luv.select = False
if square: FollowActiveUV(operator, me, targetFace, selFaces, 'EVEN')
else: FollowActiveUV(operator, me, targetFace, selFaces)
if noEdge is False:
#edge has ripped so we connect it back
for ev in edgeVerts:
key = (round(ev.uv.x, precision), round(ev.uv.y, precision))
if key in vertsDict:
ev.uv = vertsDict[key][0].uv
ev.select = True
return SuccessFinished(me, startTime)
'''def ScaleSelection(factor, pivot = 'CURSOR'):
last_pivot = bpy.context.space_data.pivot_point
bpy.context.space_data.pivot_point = pivot
bpy.ops.transform.resize(value=(factor, factor, factor), constraint_axis=(Fa
=
=
=
=
lucv.uv
rucv.uv
rdcv.uv
ldcv.uv
if (startv == lucv):
finalScaleX = hypotVert(lucv, rucv)
finalScaleY = hypotVert(lucv, ldcv)
currRowX = lucv.x
currRowY = lucv.y
elif (startv == rucv):
finalScaleX = hypotVert(rucv, lucv)
finalScaleY = hypotVert(rucv, rdcv)
currRowX = rucv.x - finalScaleX
currRowY = rucv.y
elif (startv == rdcv):
finalScaleX = hypotVert(rdcv, ldcv)
finalScaleY = hypotVert(rdcv, rucv)
currRowX = rdcv.x - finalScaleX
currRowY = rdcv.y + finalScaleY
else:
finalScaleX = hypotVert(ldcv, rdcv)
#dict
for f in faces_a:
for l in f.loops:
l_edge = l.edge
if (l_edge.is_manifold is True) and (l_edge.seam is False):
l_other = l.link_loop_radial_next
f_other = l_other.face
if not f_other.tag:
yield (f, l, f_other)
f_other.tag = True
faces_b.append(f_other)
# swap
faces_a, faces_b = faces_b, faces_a
faces_b.clear()
def walk_edgeloop(l):
"""
Could make this a generic function
"""
e_first = l.edge
e = None
while True:
e = l.edge
yield e
# don't step past non-manifold edges
if e.is_manifold:
# welk around the quad and then onto the next face
l = l.link_loop_radial_next
if len(l.face.verts) == 4:
l = l.link_loop_next.link_loop_next
if l.edge is e_first:
break
else:
break
else:
break
def extrapolate_uv(fac,
l_a_outer, l_a_inner,
l_b_outer, l_b_inner):
l_b_inner[:] = l_a_inner
l_b_outer[:] = l_a_inner + ((l_a_inner - l_a_outer) * fac)
def apply_uv(f_prev, l_prev, f_next):
l_a = [None, None, None, None]
l_b = [None, None, None, None]
l_a[0]
l_a[1]
l_a[2]
l_a[3]
#
#
#
#
#
#
#
#
=
=
=
=
l_prev
l_a[0].link_loop_next
l_a[1].link_loop_next
l_a[2].link_loop_next
l_b
+-----------+
|(3)
|(2)
|
|
|l_next(0) |(1)
+-----------+
^
l_a |
#
#
#
#
#
#
+-----------+
|l_prev(0) |(1)
|
(f)
|
|(3)
|(2)
+-----------+
copy from this face to the one above.
in verts:
= v.uv
= round(v.x, precision)
= round(v.y, precision)
else:
for v in verts:
x = round(v.uv.x, precision)
y = round(v.uv.y, precision)
for vert in vertsDict[(x,y)]:
vert.uv.x = currentX
vert.uv.y = currentY
currentY = currentY - finalScale
return
def VertsDictForLine(uv_layer, bm, selVerts, vertsDict):
for f in bm.faces:
for l in f.loops:
luv = l[uv_layer]
if luv.select is True:
x = round(luv.uv.x, precision)
y = round(luv.uv.y, precision)
vertsDict[(x, y)].append(luv)
return
def ScaleTo0OnAxisAndCursor(filteredVerts, vertsDict, startv = None, horizontal
= None):
verts = filteredVerts
verts.sort(key=lambda x: x.uv[0])
#sort by .x
first = verts[0]
last = verts[len(verts)-1]
if horizontal is None:
horizontal = True
if ((last.uv.x - first.uv.x) >0.0009):
slope = (last.uv.y - first.uv.y)/(last.uv.x - first.uv.x)
if (slope > 1) or (slope <-1):
horizontal = False
else:
horizontal = False
if horizontal is True:
if startv is None:
startv = first
SetAll2dCursorsTo(startv.uv.x, startv.uv.y)
#scale to 0 on Y
ScaleTo0('Y')
return
else:
verts.sort(key=lambda x: x.uv[1]) #sort by .y
verts.reverse()
#reverse because y values drop from up to down
first = verts[0]
last = verts[len(verts)-1]
if startv is None:
startv = first
SetAll2dCursorsTo(startv.uv.x, startv.uv.y)
#scale to 0 on X
ScaleTo0('X')
return
def ScaleTo0(axis):
last_area = bpy.context.area.type
bpy.context.area.type = 'IMAGE_EDITOR'
last_pivot = bpy.context.space_data.pivot_point
bpy.context.space_data.pivot_point = 'CURSOR'
for area in bpy.context.screen.areas:
if area.type == 'IMAGE_EDITOR':
if axis is 'Y':
bpy.ops.transform.resize(value=(1, 0, 1), constraint_axis=(False
, True, False), constraint_orientation='GLOBAL', mirror=False, proportional='DIS
ABLED', proportional_edit_falloff='SMOOTH', proportional_size=1)
else:
bpy.ops.transform.resize(value=(0, 1, 1), constraint_axis=(True,
False, False), constraint_orientation='GLOBAL', mirror=False, proportional='DIS
ABLED', proportional_edit_falloff='SMOOTH', proportional_size=1)
bpy.context.space_data.pivot_point = last_pivot
return
def hypotVert(v1, v2):
hyp = hypot(v1.x - v2.x, v1.y - v2.y)
return hyp
def Corners(corners):
firstHighest = corners[0]
for c in corners:
if c.uv.y > firstHighest.uv.y:
firstHighest = c
corners.remove(firstHighest)
secondHighest = corners[0]
for c in corners:
if (c.uv.y > secondHighest.uv.y):
secondHighest = c
if firstHighest.uv.x < secondHighest.uv.x:
leftUp = firstHighest
rightUp = secondHighest
else:
leftUp = secondHighest
rightUp = firstHighest
corners.remove(secondHighest)
firstLowest = corners[0]
secondLowest = corners[1]
if firstLowest.uv.x < secondLowest.uv.x:
leftDown = firstLowest
rightDown = secondLowest
else:
leftDown = secondLowest
rightDown = firstLowest
return leftUp, leftDown, rightUp, rightDown
def ImageRatio():
ratioX, ratioY = 256,256
for a in bpy.context.screen.areas:
if a.type == 'IMAGE_EDITOR':
img = a.spaces[0].image
if img is not None and img.size[0] is not 0:
ratioX, ratioY = img.size[0], img.size[1]
break
return ratioX, ratioY
def CursorClosestTo(verts, allowedError = 0.025):
ratioX, ratioY = ImageRatio()
#any length that is certantly not smaller than distance of the closest
min = 1000
minV = verts[0]
for v in verts:
if v is None: continue
for area in bpy.context.screen.areas:
if area.type == 'IMAGE_EDITOR':
loc = area.spaces[0].cursor_location
hyp = hypot(loc.x/ratioX -v.uv.x, loc.y/ratioY -v.uv.y)
if (hyp < min):
min = hyp
minV = v
if min is not 1000: return minV
return None
def SetAll2dCursorsTo(x,y):
last_area = bpy.context.area.type
bpy.context.area.type = 'IMAGE_EDITOR'
bpy.ops.uv.cursor_set(location=(x, y))
bpy.context.area.type = last_area
return
def RotateSelected(angle, pivot = None):
if pivot is None:
pivot = "MEDIAN"
last_area = bpy.context.area.type
bpy.context.area.type = 'IMAGE_EDITOR'
last_pivot = bpy.context.space_data.pivot_point
bpy.context.space_data.pivot_point = pivot
for area in bpy.context.screen.areas:
if area.type == 'IMAGE_EDITOR':
bpy.ops.transform.rotate(value=radians(angle), axis=(-0, -0, -1), co
nstraint_axis=(False, False, False), constraint_orientation='LOCAL', mirror=Fals
e, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_siz
e=1)
break
bpy.context.space_data.pivot_point = last_pivot
bpy.context.area.type = last_area
return
def AreVertsQuasiEqual(v1, v2, allowedError = 0.0009):
if abs(v1.uv.x -v2.uv.x) < allowedError and abs(v1.uv.y -v2.uv.y) < allowedE
rror:
return True
return False
def RipUvFaces(context, operator):
startTime = time.clock()
obj = context.active_object
me = obj.data
bm = bmesh.from_edit_mesh(me)
uv_layer = bm.loops.layers.uv.verify()
bm.faces.layers.tex.verify() # currently blender needs both layers.
selFaces = []
for f in bm.faces:
isFaceSel = True
for l in f.loops:
luv = l[uv_layer]
if luv.select is False:
isFaceSel = False
break
if isFaceSel is True:
selFaces.append(f)
if len(selFaces) is 0:
target = None
for f in bm.faces:
for l in f.loops:
luv = l[uv_layer]
if luv.select is True:
target = luv
break
if target is not None: break
for f in bm.faces:
for l in f.loops:
luv = l[uv_layer]
luv.select = False
target.select = True
return SuccessFinished(me, startTime)
DeselectAll()
for sf in selFaces:
for l in sf.loops:
luv = l[uv_layer]
luv.select = True
return SuccessFinished(me, startTime)
def JoinUvFaces(context, operator):
startTime = time.clock()
obj = context.active_object
me = obj.data
bm = bmesh.from_edit_mesh(me)
uv_layer = bm.loops.layers.uv.verify()
bm.faces.layers.tex.verify() # currently blender needs both layers.
vertsDict = defaultdict(list)
#dict
class UvSquaresByShape(bpy.types.Operator):
"""Reshapes UV faces to a grid with respect to shape by length of edges arou
nd selected corner"""
bl_idname = "uv.uv_squares_by_shape"
bl_label = "UVs to grid with respect to shape"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.mode == 'EDIT_MESH')
def execute(self, context):
main(context, self)
return {'FINISHED'}
class RipFaces(bpy.types.Operator):
"""Rip UV faces apart"""
bl_idname = "uv.uv_face_rip"
bl_label = "UV face rip"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.mode == 'EDIT_MESH')
def execute(self, context):
RipUvFaces(context, self)
return {'FINISHED'}
class JoinFaces(bpy.types.Operator):
"""Join selected UV faces to closest nonselected vertices"""
bl_idname = "uv.uv_face_join"
bl_label = "UV face join"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.mode == 'EDIT_MESH')
def execute(self, context):
JoinUvFaces(context, self)
return {'FINISHED'}
class SnapToAxis(bpy.types.Operator):
"""Snap sequenced vertices to Axis"""
bl_idname = "uv.uv_snap_to_axis"
bl_label = "UV snap vertices to axis"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.mode == 'EDIT_MESH')
def execute(self, context):
main(context, self)
return {'FINISHED'}
class SnapToAxisWithEqual(bpy.types.Operator):
"""Snap sequenced vertices to Axis with Equal Distance between"""
bl_idname = "uv.uv_snap_to_axis_and_equal"
bl_label = "UV snap vertices to axis with equal distance between"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.mode == 'EDIT_MESH')
def execute(self, context):
main(context, self)
main(context, self)
return {'FINISHED'}
addon_keymaps = []
def menu_func_uv_squares(self, context): self.layout.operator(UvSquares.bl_idnam
e)
def menu_func_uv_squares_by_shape(self, context): self.layout.operator(UvSquares
ByShape.bl_idname)
def menu_func_face_rip(self, context): self.layout.operator(RipFaces.bl_idname)
def menu_func_face_join(self, context): self.layout.operator(JoinFaces.bl_idname
)
class UvSquaresPanel(bpy.types.Panel):
"""UvSquares Panel"""
bl_label = "UV Squares"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'TOOLS'
def draw(self, context):
layout = self.layout
row = layout.row()
row.label(text="Select Sequenced Vertices to:")
split = layout.split()
col = split.column(align=True)
col.operator(SnapToAxis.bl_idname, text="Snap to Axis (X or Y)", icon =
"ARROW_LEFTRIGHT")
col.operator(SnapToAxisWithEqual.bl_idname, text="Snap with Equal Distan
ce", icon = "ALIGN")
row = layout.row()
row.label(text="Convert \"Rectangle\" (4 corners):")
split = layout.split()
col = split.column(align=True)
col.operator(UvSquaresByShape.bl_idname, text="To Grid By Shape", icon =
"GRID")
col.operator(UvSquares.bl_idname, text="To Square Grid", icon = "UV_FACE
SEL")
split = layout.split()
col = split.column(align=True)
row = col.row(align=True)
row = layout.row()
row.label(text="Select Faces or Vertices to:")
split = layout.split()
col = split.column(align=True)
row = col.row(align=True)
def unregister():
bpy.utils.unregister_class(UvSquaresPanel)
bpy.utils.unregister_class(UvSquares)
bpy.utils.unregister_class(UvSquaresByShape)
bpy.utils.unregister_class(RipFaces)
bpy.utils.unregister_class(JoinFaces)
bpy.utils.unregister_class(SnapToAxis)
bpy.utils.unregister_class(SnapToAxisWithEqual)
bpy.types.IMAGE_MT_uvs.remove(menu_func_uv_squares)
bpy.types.IMAGE_MT_uvs.remove(menu_func_uv_squares_by_shape)
bpy.types.IMAGE_MT_uvs.remove(menu_func_face_rip)
bpy.types.IMAGE_MT_uvs.remove(menu_func_face_join)
# handle the keymap
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
# clear the list
addon_keymaps.clear()
if __name__ == "__main__":
register()