#!BPY

"""
Name: 'Similar to Active'
Blender: 234
Group: 'FaceSelect'
Tooltip: 'Select faces that match a given attribute of the active face'
"""

__author__ = "Campbell Barton"
__url__ = ["blender", "elysiun"]
__version__ = "1.0"

__bpydoc__ = """\
This script selects faces matching a given attribute of the currently
active face.

Usage:

Enter "UV Face Select" mode and make the desired face active.  Then run this
script and choose the selection rule: by same (or similar for some itens):

- material;<br>
- texture image;<br>
- mode;<br>
- vertex colors;<br>
- uv coordinates;<br>
- area;<br>
- proportions;<br>
- normal vector;<br>
- coplanar.

Another menu will ask if the script should add, subtract, overwrite or
overwrite inverse of current current selection.  For some choices like vcolors,
area, etc., a pop-up will ask for a maximum threshold value.

Notes:<br>
   Again, to select / deselect faces, enter "UV Face Select" mode.  This is not
the same as selecting faces in edit mode (new feature in Blender 2.35).
"""

# $Id$
#
#===============================================#
# Sel Same script 1.0 by Campbell Barton        #
# email me ideasman@linuxmail.org               #
#===============================================#


# -------------------------------------------------------------------------- 
# Sel Same Face 1.0 By Campbell Barton (AKA Ideasman)
# -------------------------------------------------------------------------- 
# ***** 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. 
# 
# ***** END GPL LICENCE BLOCK ***** 
# -------------------------------------------------------------------------- 



from Blender import *
from Blender.Mathutils import DotVecs, Vector
from math import sqrt


#====================================#
# Sanity checks                      #
#====================================#
def error(str):
	Draw.PupMenu('ERROR: '+str)
af = None 
selection = Object.GetSelected()
if len(selection) == 0:
  error('No object selected')
else:
  object = Object.GetSelected()[0]
  if object.getType() != 'Mesh':
    error('Active object must be a mesh')
  else:
    mesh = object.getData()
    
    # We have a mesh so find AF.
    af = mesh.getActiveFace()
    if af: af = mesh.faces[af]

if af == None:
  error('No active face')

else: # Okay everything seems sane
  
  #=====================================
  # Popup menu to select the functions #
  #====================================#
  method = Draw.PupMenu(\
  'Selection Attribute%t|\
  Material|\
  UV Image|\
  Face Mode|\
  Vertex Colours|\
  UV Coordinates|\
  Area|\
  Edge Proportions|\
  Normal Vector|\
  Coplanar|')
  
  if method != -1:
    #================================================#
    # Do we add, seb or set to the existing face sel #
    #================================================#
    faceOp = Draw.PupMenu(\
    'Active Face Match%t|\
    Add to Selection|\
    Subtract From Selection |\
    Overwrite Selection|\
    Overwrite Selection Inverse|')
    
    if faceOp != -1:
      
      def setFSel(f):
        if faceOp == 1 or faceOp == 3:
          f.flag |= NMesh.FaceFlags['SELECT'] # will set selection
        elif faceOp == 2 or faceOp ==4:
          f.flag &=~NMesh.FaceFlags['SELECT'] # will unselect, note the '~' to invert bitflags
      
      def setFUnSel(f):
        if faceOp == 3:
          f.flag &=~NMesh.FaceFlags['SELECT'] # will unselect, note the '~' to invert bitflags
        elif faceOp == 4:
          f.flag |= NMesh.FaceFlags['SELECT'] # will set selection
      
      
      
      #================#
      # Math functions #
      #================#
      def compare(f1, f2, limit):
        if f1 + limit > f2 and f1 - limit < f2:
          return 1
        return 0
      
      def compare2(v1, v2, limit):
        if v1[0] + limit > v2[0] and v1[0] - limit < v2[0]:
            if v1[1] + limit > v2[1] and v1[1] - limit < v2[1]:
              return 1
        return 0
      
      def compare3(v1, v2, limit):
        if v1[0] + limit > v2[0] and v1[0] - limit < v2[0]:
            if v1[1] + limit > v2[1] and v1[1] - limit < v2[1]:
              if v1[2] + limit > v2[2] and v1[2] - limit < v2[2]:
                return 1
        return 0
      
      def colCompare(v1, v2, limit):
        # Simple test first
        if v1.r == v2.r:
          if v1.g == v2.g:
            if v1.b == v2.b:
              return 1  
        # Now a test that uses the limit.
        limit = int(limit * 255)
        if v1.r + limit >= v2.r and v1.r - limit <= v2.r:
          if v1.g + limit >= v2.g and v1.g - limit <= v2.g:
            if v1.b + limit >= v2.b and v1.b - limit <= v2.b:
              return 1
        return 0
      
      # Makes sure face 2 has all the colours of face 1
      def faceColCompare(f1, f2, limit):
        avcolIdx = 0
        while avcolIdx < len(f1.col):
          match = 0
          
          vcolIdx = 0
          while vcolIdx < len(f2.col):
            if colCompare(f1.col[avcolIdx], f2.col[vcolIdx], limit):
              match = 1
              break
            vcolIdx += 1
          if match == 0: # premature exit if a motch not found
            return 0
          avcolIdx += 1
        return 1
      
      # Makes sure face 2 has matching UVs within the limit.
      def faceUvCompare(f1, f2, limit):
        for auv in f1.uv:
          match = 0
          for uv in f2.uv:
            if compare2(auv, uv, limit):
              match = 1
              break
          if match == 0: # premature exit if a motch not found
            return 0
        return 1
      
      
      def measure(v1, v2):
        return Mathutils.Vector([v1[0]-v2[0], v1[1] - v2[1], v1[2] - v2[2]]).length
      
      def triArea2D(v1, v2, v3):
        e1 = measure(v1, v2)  
        e2 = measure(v2, v3)  
        e3 = measure(v3, v1)  
        p = e1+e2+e3
        return 0.25 * sqrt(p*(p-2*e1)*(p-2*e2)*(p-2*e3))
      #====================#
      # End Math Functions #
      #====================#
      
      
      #=============================#
      # Blender functions/shortcuts #
      #=============================#
      def getLimit(text):
        return Draw.PupFloatInput(text, 0.1, 0.0, 1.0, 0.1, 3)
      
      def faceArea(f):
        if len(f.v) == 4:
          return triArea2D(f.v[0].co, f.v[1].co, f.v[2].co) + triArea2D(f.v[0].co, f.v[2].co, f.v[3].co)
        elif len(f.v) == 3:
          return triArea2D(f.v[0].co, f.v[1].co, f.v[2].co)
        
      def getEdgeLengths(f):
        if len(f.v) == 4:
          return (measure(f.v[0].co, f.v[1].co), measure(f.v[1].co, f.v[2].co), measure(f.v[2].co, f.v[3].co) , measure(f.v[3].co, f.v[0].co) )
        elif len(f.v) == 3:
          return (measure(f.v[0].co, f.v[1].co), measure(f.v[1].co, f.v[2].co), measure(f.v[2].co, f.v[0].co) )
      
      def faceCent(f):
        x = y = z = 0
        for v in f.v:
          x += v.co[0]
          y += v.co[1]
          z += v.co[2]
        x = x/len(f.v)
        y = y/len(f.v)
        z = z/len(f.v)
        return Vector([x, y, z])
      
      #========================================#
      # Should we bother computing this faces  #
      #========================================#
      def fShouldCompare(f):
        # Only calculate for faces that will be affected.
        if len(f.v) < 3: # cant be an edge
          return 0
        elif faceOp == 1 and f.flag == 1:
          return 0
        elif faceOp == 0 and f.flag == 0:
          return 0
        elif f.flag == 64: # Ignore hidden
          return 0
        return 1
        
      #=======================================#
      # Sel same funcs as called by the menus #
      #=======================================#
      def get_same_mat():
        for f in mesh.faces:
          if fShouldCompare(f):
            if af.mat == f.mat: setFSel(f)
            else:               setFUnSel(f)
      
      def get_same_image():
        if mesh.hasFaceUV() == 0:
          error('mesh has no uv image')
        else:
          for f in mesh.faces:
            if fShouldCompare(f):
              if af.image == f.image: setFSel(f)
              else:                   setFUnSel(f)
      
      def get_same_mode():
        for f in mesh.faces:
          if fShouldCompare(f):
            if af.mode == f.mode: setFSel(f)
            else:                 setFUnSel(f)
      
      def get_same_vcol(limit):
        for f in mesh.faces:
          if fShouldCompare(f):
            if faceColCompare(f, af, limit) and faceColCompare(af, f, limit) :
              setFSel(f)
            else:
              setFUnSel(f)
      
      def get_same_uvco(limit):
        for f in mesh.faces:
          if fShouldCompare(f):
            if faceUvCompare(af, f, limit): setFSel(f)
            else:                           setFUnSel(f)
      
      def get_same_area(limit):
        afArea = faceArea(af)
        limit = limit * afArea # Make the lomot proportinal to the 
        for f in mesh.faces:
          if fShouldCompare(f):
            if compare(afArea, faceArea(f), limit): setFSel(f)
            else:                                   setFUnSel(f)
      
      def get_same_prop(limit):
        # Here we get the perimeter and use it for a proportional limit modifier.
        afEdgeLens = getEdgeLengths(af)
        perim = 0
        for e in afEdgeLens:
          perim += e
      
        limit = limit * perim
        for f in mesh.faces:
          if fShouldCompare(f):
            for ae in afEdgeLens:
              match = 0
              for e in getEdgeLengths(f):
                if compare(ae, e, limit):
                  match = 1
                  break
              if not match:
                break
           
            if match: setFSel(f)
            else:     setFUnSel(f)
      
      def get_same_normal(limit):    
        limit = limit * 2
        for f in mesh.faces:
          if fShouldCompare(f):
            if compare3(af.no, f.no, limit): setFSel(f)
            else:                            setFUnSel(f)
      
      def get_same_coplaner(limit):
        nlimit = limit * 2 # * 1 # limit for normal test
        climit = limit * 3 # limit for coplaner test.
        afCent = faceCent(af)
        for f in mesh.faces:
          if fShouldCompare(f):
            match = 0
            if compare3(af.no, f.no, nlimit):
              fCent = faceCent(f)
              if abs(DotVecs(Vector([af.no[0], af.no[1], af.no[2]]), afCent ) - DotVecs(Vector([af.no[0], af.no[1], af.no[2]]), fCent )) <= climit:
                match = 1
            if match:
              setFSel(f)
            else:
              setFUnSel(f)
      
      #=====================#
      # End Sel same funcs  #
      #=====================#
      limit = 1 # some of these dont use the limit so it needs to be set, to somthing.
      # act on the menu item selected
      if method == 1: # Material
        get_same_mat()
      elif method == 2: # UV Image
        get_same_image()
      elif method == 3: # mode
        get_same_mode()
      elif method == 4: # vertex colours
        limit = getLimit('vert col limit: ')
        if limit != None:
          get_same_vcol(limit)
      elif method == 5: # UV-coords
        limit = getLimit('uv-coord limit: ')
        if limit != None:
          get_same_uvco(limit)
      elif method == 6: # area
        limit = getLimit('area limit: ')
        if limit != None:
          get_same_area(limit)
      elif method == 7: # proportions
        limit = getLimit('proportion limit: ')
        if limit != None:
          get_same_prop(limit)
      elif method == 8: # normal
        limit = getLimit('normal limit: ')
        if limit != None:
          get_same_normal(limit)
      elif method == 9: # coplaner
        limit = getLimit('coplanar limit: ')
        if limit != None:
          get_same_coplaner(limit)
      
      # If limit is not set then dont bother
      if limit != None:
        mesh.update(0)
