#!BPY
# -*- coding: latin-1 -*-
"""
Name: 'NormalSmooth'
Blender: 248
Group: 'Misc'
Tooltip: 'Smooth the selected verts in your mesh'
"""

__author__ = ["macouno"]
__url__ = ("http://www.alienhelpdesk.com")
__version__ = "6"
__bpydoc__ = """\

NormalSmooth

This script uses vertex normals to smooth out a mesh.
It works fastest / best if you run in on only one or two edge loops (selections) at a time.
If you have a larger selected surface the script will get slower and may create a 'dip' in the middle of your selection.

v2: Attempt to get neater end result
v3: Simplified code, can run on an entire mesh now, probably needs more iterations
v4: Switched from iterations to steps to solve the dip issue
v5: Went back to v2 code but did make that work with entire meshes
v6: Made flat surfaces be displaced in their entirety in the v3 code

"""

# ***** BEGIN GPL LICENSE BLOCK *****
#
# Script copyright (C) macouno 2007
#
# 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
# --------------------------------------------------------------------------

from Blender import Window, Scene, Draw, Mathutils
import BPyMesh

## A couple of global variables
Iterations = 1	#Standard nr of iterations

## A few extra vars
vNew = {}
areas = []
inArea = []


## Get the angle between two vectors
def VtoVAngle(vec1, vec2):
	if (vec1.length == 0.0 or vec2.length == 0.0): return 0.0
	else: return Mathutils.AngleBetweenVecs(vec1, vec2)


## Rotate one vector (vec1) towards another (vec2)
## (deg = ammount of degrees to rotate)
def RotVtoV(vec1, vec2, deg):
	cross = Mathutils.CrossVecs(vec1,vec2)
	mat = Mathutils.RotationMatrix(deg, 3, 'r', cross)
	return (vec1 * mat)


## Get the averase location in a list of locations
def GetAverage(vList):
		v = Mathutils.Vector()
		for m in vList:
			v += m
		v /= len(vList)
		return v


## Get all the verts that have matching normal angles and are connected by faces
def GetConnected(me, v1, matched):

	global areas, inArea

	v1in = v1.index

	# Find faces 
	for f in me.faces:
		if v1 in f.verts:
			for v2 in f.verts:
				v2in = v2.index
				## Make sure the verts arent the same and that v2 isn't already in the area
				if not v1in == v2in and not v2in in inArea:
					
					# If the angle between the normals is 0 we have found a surface!
					if VtoVAngle(v1.no, v2.no) < 0.2:
					
						if not matched:
							matched += 1
							areas.append([v1in, v2in])
							inArea.append(v1in)
							inArea.append(v2in)
						else:
							areas[(len(areas)-1)].append(v2in)
							inArea.append(v2in)
							
						GetConnected(me, v2, 1)


## Find larger areas with matching normals
def GetAreas(me):
	
	global areas, inArea
	areas = []
	inArea = []
	
	## Find a selected vert that isn't in an area
	for v1 in me.verts:
		if v1.sel and not v1.index in inArea:
			GetConnected(me, v1, 0)


## Move the vert relative to it's neighbouring normals
def SmoothVert(me, v1, vCh):

	v1in = v1.index
	v1co = Mathutils.Vector(v1.co)
	vCh.append(v1in)
	vList = []

	for f in me.faces:
		if v1 in f:
			for v2 in f:
				v2in = v2.index
				if not v2in in vCh:
					
					# Since we're checking this vert already, add it to the list of checked verts
					vCh.append(v2in)
					
					v2co = Mathutils.Vector(v2.co)
					
					# Get the vector from one vert to the other
					vTov = v1co - v2co
					
					# Get half the distance from one vert to the other
					vLen = vTov.length * 0.514
					
					# Get the normal rotated 90 degrees towards the original vert
					vNor = RotVtoV(v2.no, vTov.normalize(), 90)
					
					# Make the new rotated normal the hald distance long
					vNor = vNor.normalize() * vLen
					
					# Find the location the vector points at
					vNew = v2co + vNor
				
					vList.append(vNew)
	
	# Go get the average position change and return
	if len(vList):
		return GetAverage(vList)

	return 0


def MainScript(me):

	global vNew, areas, inArea
	vNew = {}
	
	# Get all the areas that have verts with the same normal
	GetAreas(me)
	
	## If there are areas get the new coords for all verts in em
	if len(areas):
		for a in areas:
			vList = []
			coords = {}
			for v in a:
				vert = me.verts[v]
				coords[v] = Mathutils.Vector(vert.co)
				smoothCo = SmoothVert(me, vert, list(a))
				if smoothCo:
					vList.append(smoothCo - coords[v])
				
			## If no new coords were found no need to continue
			if len(vList):
				vN = GetAverage(vList)
				
				for v in a:
					vNew[v] = vN + coords[v]

	# Loop through the non area verts and get the new coords
	for v in me.verts:
		if v.sel and not v.index in inArea:
			vNew[v.index] = SmoothVert(me, v, [])

	# Implement the changes
	if len(vNew):
		for i in vNew.keys():
			if vNew[i]:
				me.verts[i].co = vNew[i]


## Lets get all the actual data we need and see if we can smooth something
def RunSmoothing():

	global Iterations

	scn = Scene.GetCurrent()
	ob = scn.getActiveObject()

	## If the current object is no mesh then we can quit
	if not ob or ob.getType() != 'Mesh':
		Draw.PupMenu('Error, no active mesh object, aborting.')
		return
		
	me = ob.getData(mesh=1)
	del ob
	
	## If the current object is no mesh then we can quit
	if not len(me.faces):
		Draw.PupMenu('Error, no faces found in this mesh object, aborting.')
		return
	
	## Draw the popup menu that sets the number of iterations
	Iterations = Draw.PupIntInput('iterations:', 1, 1, 1000) 
	
	BPyMesh.meshCalcNormals(me)
	for i in range(Iterations):
		if i < Iterations:
			MainScript(me)
			
	me.update()
	scn.update()
	del scn, me

	
## Set up the scene/windows and reset em when done
def  Initialise():

	Window.WaitCursor(1)

	emode = int(Window.EditMode())
	if emode: Window.EditMode(0)

	RunSmoothing()

	Window.Redraw(Window.Types.VIEW3D)
	
	if emode: Window.EditMode(1)
	
	Window.WaitCursor(0)
	
## Initialise the script
Initialise()	