
Feature request during bconf, makes sense to have it even as an hack for now, since this is probably one of the most common use cases. This should be redone in bmesh once we have proper custom noramls handling in edit mode...
251 lines
7.6 KiB
Python
251 lines
7.6 KiB
Python
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
# <pep8-80 compliant>
|
|
|
|
import bpy
|
|
from bpy.types import Operator
|
|
|
|
from bpy.props import EnumProperty, IntProperty
|
|
|
|
|
|
class MeshMirrorUV(Operator):
|
|
"""Copy mirror UV coordinates on the X axis based on a mirrored mesh"""
|
|
bl_idname = "mesh.faces_mirror_uv"
|
|
bl_label = "Copy Mirrored UV coords"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
direction = EnumProperty(
|
|
name="Axis Direction",
|
|
items=(('POSITIVE', "Positive", ""),
|
|
('NEGATIVE', "Negative", "")),
|
|
)
|
|
|
|
precision = IntProperty(
|
|
name="Precision",
|
|
description=("Tolerance for finding vertex duplicates"),
|
|
min=1, max=16,
|
|
soft_min=1, soft_max=16,
|
|
default=3,
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
obj = context.active_object
|
|
return (obj and obj.type == 'MESH' and obj.data.uv_textures.active)
|
|
|
|
def execute(self, context):
|
|
DIR = (self.direction == 'NEGATIVE')
|
|
precision = self.precision
|
|
double_warn = 0
|
|
|
|
ob = context.active_object
|
|
is_editmode = (ob.mode == 'EDIT')
|
|
if is_editmode:
|
|
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
|
|
|
mesh = ob.data
|
|
|
|
# mirror lookups
|
|
mirror_gt = {}
|
|
mirror_lt = {}
|
|
|
|
vcos = (v.co.to_tuple(precision) for v in mesh.vertices)
|
|
|
|
for i, co in enumerate(vcos):
|
|
if co[0] >= 0.0:
|
|
double_warn += co in mirror_gt
|
|
mirror_gt[co] = i
|
|
if co[0] <= 0.0:
|
|
double_warn += co in mirror_lt
|
|
mirror_lt[co] = i
|
|
|
|
vmap = {}
|
|
for mirror_a, mirror_b in ((mirror_gt, mirror_lt),
|
|
(mirror_lt, mirror_gt)):
|
|
for co, i in mirror_a.items():
|
|
nco = (-co[0], co[1], co[2])
|
|
j = mirror_b.get(nco)
|
|
if j is not None:
|
|
vmap[i] = j
|
|
|
|
polys = mesh.polygons
|
|
loops = mesh.loops
|
|
uv_loops = mesh.uv_layers.active.data
|
|
nbr_polys = len(polys)
|
|
|
|
mirror_pm = {}
|
|
pmap = {}
|
|
puvs = [None] * nbr_polys
|
|
puvs_cpy = [None] * nbr_polys
|
|
puvsel = [None] * nbr_polys
|
|
pcents = [None] * nbr_polys
|
|
vidxs = [None] * nbr_polys
|
|
for i, p in enumerate(polys):
|
|
lstart = lend = p.loop_start
|
|
lend += p.loop_total
|
|
puvs[i] = tuple(uv.uv for uv in uv_loops[lstart:lend])
|
|
puvs_cpy[i] = tuple(uv.copy() for uv in puvs[i])
|
|
puvsel[i] = (False not in
|
|
(uv.select for uv in uv_loops[lstart:lend]))
|
|
# Vert idx of the poly.
|
|
vidxs[i] = tuple(l.vertex_index for l in loops[lstart:lend])
|
|
pcents[i] = p.center
|
|
# Preparing next step finding matching polys.
|
|
mirror_pm[tuple(sorted(vidxs[i]))] = i
|
|
|
|
for i in range(nbr_polys):
|
|
# Find matching mirror poly.
|
|
tvidxs = [vmap.get(j) for j in vidxs[i]]
|
|
if None not in tvidxs:
|
|
tvidxs.sort()
|
|
j = mirror_pm.get(tuple(tvidxs))
|
|
if j is not None:
|
|
pmap[i] = j
|
|
|
|
for i, j in pmap.items():
|
|
if not puvsel[i] or not puvsel[j]:
|
|
continue
|
|
elif DIR == 0 and pcents[i][0] < 0.0:
|
|
continue
|
|
elif DIR == 1 and pcents[i][0] > 0.0:
|
|
continue
|
|
|
|
# copy UVs
|
|
uv1 = puvs[i]
|
|
uv2 = puvs_cpy[j]
|
|
|
|
# get the correct rotation
|
|
v1 = vidxs[j]
|
|
v2 = tuple(vmap[k] for k in vidxs[i])
|
|
|
|
if len(v1) == len(v2):
|
|
for k in range(len(v1)):
|
|
k_map = v1.index(v2[k])
|
|
uv1[k].xy = - (uv2[k_map].x - 0.5) + 0.5, uv2[k_map].y
|
|
|
|
if is_editmode:
|
|
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
|
|
|
|
if double_warn:
|
|
self.report({'WARNING'},
|
|
"%d duplicates found, mirror may be incomplete" %
|
|
double_warn)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class MeshSelectNext(Operator):
|
|
"""Select the next element (using selection order)"""
|
|
bl_idname = "mesh.select_next_item"
|
|
bl_label = "Select Next Element"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return (context.mode == 'EDIT_MESH')
|
|
|
|
def execute(self, context):
|
|
import bmesh
|
|
from .bmesh import find_adjacent
|
|
|
|
obj = context.active_object
|
|
me = obj.data
|
|
bm = bmesh.from_edit_mesh(me)
|
|
|
|
if find_adjacent.select_next(bm, self.report):
|
|
bm.select_flush_mode()
|
|
bmesh.update_edit_mesh(me, False)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class MeshSelectPrev(Operator):
|
|
"""Select the next element (using selection order)"""
|
|
bl_idname = "mesh.select_prev_item"
|
|
bl_label = "Select Previous Element"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return (context.mode == 'EDIT_MESH')
|
|
|
|
def execute(self, context):
|
|
import bmesh
|
|
from .bmesh import find_adjacent
|
|
|
|
obj = context.active_object
|
|
me = obj.data
|
|
bm = bmesh.from_edit_mesh(me)
|
|
|
|
if find_adjacent.select_prev(bm, self.report):
|
|
bm.select_flush_mode()
|
|
bmesh.update_edit_mesh(me, False)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# XXX This is hackish (going forth and back from Object mode...), to be redone once we have proper support of
|
|
# custom normals in BMesh/edit mode.
|
|
class MehsSetNormalsFromFaces(Operator):
|
|
"""Set the custom vertex normals from the selected faces ones"""
|
|
bl_idname = "mesh.set_normals_from_faces"
|
|
bl_label = "Set Normals From Faces"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return (context.mode == 'EDIT_MESH' and context.edit_object.data.polygons)
|
|
|
|
def execute(self, context):
|
|
import mathutils
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
obj = context.active_object
|
|
me = obj.data
|
|
|
|
v2nors = {}
|
|
for p in me.polygons:
|
|
if not p.select:
|
|
continue
|
|
for lidx, vidx in zip(p.loop_indices, p.vertices):
|
|
assert(me.loops[lidx].vertex_index == vidx)
|
|
v2nors.setdefault(vidx, []).append(p.normal)
|
|
|
|
for nors in v2nors.values():
|
|
nors[:] = [sum(nors, mathutils.Vector((0, 0, 0))).normalized()]
|
|
|
|
if not me.has_custom_normals:
|
|
me.create_normals_split()
|
|
me.calc_normals_split()
|
|
|
|
normals = []
|
|
for l in me.loops:
|
|
nor = v2nors.get(l.vertex_index, [None])[0]
|
|
if nor is None:
|
|
nor = l.normal
|
|
normals.append(nor.to_tuple())
|
|
|
|
me.normals_split_custom_set(normals)
|
|
|
|
me.free_normals_split()
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
return {'FINISHED'}
|
|
|