#!BPY

"""
Name: 'Pattern selector'
Blender: 248
Group: 'Mesh'
Tooltip: 'Select a pattern of faces in a selected faces ribbon.'
"""

__author__  = "Guillaume 'GuieA_7' Englert"
__version__ = "0.2 2009/02/13"
__url__     = "Online doc , http://www.hybird.org/~guiea_7/"
__email__   = "GuieA_7, genglert:hybird*org"
__bpydoc__  = """\
Select a pattern of faces in a selected faces ribbon/loop.<br>
How to use it:<br>
- select a faces ribbon (it can be a loop).<br>
- launch the script, and enter the pattern. The pattern should only contain '0'
and '1'. '1' means 'selected', '0' means non-selected ; the pattern can be applied
several times on the ribbon, in order to be applied on all its faces.<br>

Reverse button : reverses the order of the pattern. Example : '100' becomes '001'.<br>
Invert button : 1 becomes 0, 0 becomes 1. Example: '100' becomes '011'.<br>

<br>
Example, with the pattern '10' and a 4-faced ribbon:<br>
the pattern is applied on the faces (1, 2), then on the faces (3, 4) ; so the
faces 1 and 3 will be selected, the faces 2 and 4 won't be.
"""

################################################################################
#                                                                              #
#    GNU GPL LICENSE                                                           #
#    ---------------                                                           #
#                                                                              #
#    Copyright (C) 2008-2009: Guillaume Englert                                #
#                                                                              #
#    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.           #
#                                                                              #
################################################################################


################################################################################
# Importing modules
################################################################################

from exceptions import Exception

from Blender import Mesh
from Blender.Object import GetSelected
from Blender.Window import EditMode, GetMouseCoords
from Blender.Draw import PupMenu, UIBlock, PushButton, Toggle, String
from Blender import Registry


################################################################################
#                              EXCEPTIONS                                      #
################################################################################

class PSFatalError(Exception):
    """Fatal error, like no object selected."""
    def __init__(self, msg):
        Exception.__init__(self)
        self.msg = msg

    def __str__(self):
        return self.msg


class PSError(Exception):
    """User input error."""
    def __init__(self, msg, priority=0):
        Exception.__init__(self)
        self.msg      = msg
        self.priority = priority

    def __str__(self):
        return self.msg


################################################################################
#                             CONSTANTS                                        #
################################################################################

SCRIPTNAME  = 'PatternSelect'
PATTERN_KEY = 'pattern'
MODE_KEY    = 'mode'

EVENT_NONE   = 0
EVENT_OK     = 1
EVENT_CANCEL = 2

MODE_FACE = 0
MODE_EDGE = 1
MODE_VERT = 2

DEFAULT_PATTERN = '10'
DEFAULT_MODE = MODE_FACE

SEL_E = Mesh.EdgeFlags['SELECT']
GEOM_EVENT_BASE = 666


################################################################################
#                              CLASS                                           #
################################################################################


class Pattern(object):
    __slots__ = ('_pattern_str', '_mode')

    def __init__(self):
        self._pattern_str = DEFAULT_PATTERN
        self._mode        = DEFAULT_MODE

        #get saved pattern
        conf = Registry.GetKey(SCRIPTNAME, False)

        if conf:
            try:
                saved_str = conf[PATTERN_KEY]
                assert isinstance(saved_str, str)
                self.pattern_str = saved_str

                saved_mode = conf[MODE_KEY]
                assert isinstance(saved_mode, int)
                self.mode = saved_mode
            except Exception, e:
                print "invalid registry??: ", e

    def _set_pattern_str(self, pattern_str):
        if not pattern_str:
            raise PSError("Pattern can't be empty.")

        for c in pattern_str:
            if c not in ('0', '1'):
                raise PSError("Pattern must only contain '0' and '1'.")

        self._pattern_str = pattern_str

    pattern_str = property(lambda self: self._pattern_str, _set_pattern_str)

    def _set_mode(self, mode):
        assert MODE_FACE <= mode <= MODE_VERT
        self._mode = mode

    mode = property(lambda self: self._mode, _set_mode)

    def __iter__(self):
        while True:
            for char in iter(self._pattern_str):
                yield char == '1'

    def save(self):
        conf = {PATTERN_KEY: self._pattern_str, MODE_KEY: self._mode}
        Registry.SetKey(SCRIPTNAME, conf, False)

    def invert(self):
        self._pattern_str = ''.join('1' if char == '0' else '0' for char in self._pattern_str)

    def reverse(self):
        self._pattern_str = self._pattern_str[::-1]


class GUI(object):
    def __init__(self, pattern):
        self._pattern = pattern
        self._event = EVENT_NONE
        self._x, self._y  = GetMouseCoords()

        toggles = [0, 0, 0] #FACE/EDGE/VERTEX
        toggles[pattern.mode] = 1
        self._toggles = toggles

    def _toggle_cb(self, event, value):
        mode = event - GEOM_EVENT_BASE
        toggles = [1 - value] * 3 #1 - value ==> 1<-->0
        toggles[mode] = value
        self._toggles = toggles
        self._pattern.mode = mode

    def _pattern_str_cb(self, event, value):
        try:
            self._pattern.pattern_str = value
        except PSError, e:
            display_error(str(e))

    def _button_cb(self, event, value):
        self._event = event

    def _reverse_pattern(self, event, value):
        self._pattern.reverse()

    def _invert_pattern(self, event, value):
        self._pattern.invert()

    def _draw(self):
        h = 25; w = 180
        x = self._x
        y = self._y
        toggles = self._toggles
        bw = w // len(toggles) #button width

        base = GEOM_EVENT_BASE
        cb = self._toggle_cb
        Toggle('Face',   base+MODE_FACE, x,      y, bw, h, toggles[MODE_FACE], 'Toggle face mode',   cb)
        Toggle('Edge',   base+MODE_EDGE, x+bw,   y, bw, h, toggles[MODE_EDGE], 'Toggle edge mode',   cb)
        Toggle('Vertex', base+MODE_VERT, x+2*bw, y, bw, h, toggles[MODE_VERT], 'Toggle vertex mode', cb)

        y -= h
        String('Pattern: ', base+10, x, y, w, h, self._pattern.pattern_str, 30, '1=Selected, 0=Not selected', self._pattern_str_cb)

        y -= h
        PushButton('Reverse', base+11, x,    y, bw, h, 'Pattern start becomes end', self._reverse_pattern)
        PushButton('Invert',  base+12, x+bw, y, bw, h, "'1' <-> '0'",               self._invert_pattern)

        y -= h + 10
        cb = self._button_cb
        PushButton('Cancel', EVENT_CANCEL, x,    y, bw, h, '', cb)
        PushButton('OK',     EVENT_OK,     x+bw, y, bw, h, '', cb)

    def show(self):
        while self._event == EVENT_NONE:
            UIBlock(self._draw, 0)

        return self._event


class FastObj(object):
    def is_neighbour(self, other):
        """return: True if neighbours"""
        raise NotImplemntedError()

    def select(self, sel):
        """sel: boolean - True to select object."""
        raise NotImplemntedError()


class FastFace(FastObj):
    """Wraps Blender face to know if 2 faces are neighbours"""
    __slots__ = ('_face', '_vertset')

    def __init__(self, face):
        """face: Blender.Mesh.MFace object."""
        self._face    = face
        self._vertset = set(v.index for v in face.verts)

    def __repr__(self):
        return "<FastFace wrapping face %i>" % self._face.index

    def is_neighbour(self, other):
        return len(self._vertset & other._vertset) >= 2

    def select(self, sel):
        self._face.sel = sel


class FastEdge(FastObj):
    """Wraps Blender face to know if 2 edges are neighbours"""
    __slots__ = ('_edge', '_vertset')

    def __init__(self, edge):
        """edge: Blender.Mesh.MEdge object."""
        self._edge    = edge
        self._vertset = set(v.index for v in edge)

    def __repr__(self):
        return "<FastEdge wrapping edge %i>" % self._edge.index

    def is_neighbour(self, other):
        return len(self._vertset & other._vertset) != 0

    def select(self, sel):
        edge = self._edge
        if sel:
            edge.flag = edge.flag | SEL_E
        else:
            edge.flag = edge.flag & ~SEL_E


class FastVert(FastObj):
    """Wraps Blender face to know if 2 vertices are neighbours"""
    __slots__ = ('_vert', '_edgeset')

    def __init__(self, vert, edgeset):
        """ Constructor.
        vert: Blender.Mesh.MVert object.
        edgeset: set of indices of edges that contain this vertex.
        """
        self._vert = vert
        self._edgeset = edgeset

    def __repr__(self):
        return "<FastVert wrapping edge %i>" % self._vert.index

    def is_neighbour(self, other):
        return len(self._edgeset & other._edgeset) != 0

    def select(self, sel):
        self._vert.sel = sel


class Ribbon(object):
    __slots__ = '_ribbon'

    _no_obj_error_msg  = 'You must select at least one obj.'
    _no_loop_error_msg = 'You must select an obj ribbon/loop.'
    mesh_mode = 'UNKNOWN'

    def __init__(self, fastobjs):
        if not len(fastobjs):
            raise PSError(self._no_obj_error_msg, priority=1)

        #reference face, randomly chosen
        refobj = fastobjs.pop()

        #refface can be in the middle of the ribbon --> build_face_ribbon x 2
        ribbon = self._build_half_ribbon(refobj, fastobjs)
        ribbon.reverse()
        ribbon.append(refobj)
        ribbon.extend(self._build_half_ribbon(refobj, fastobjs))

        if fastobjs:
            raise PSError(self._no_loop_error_msg, priority=1)

        self._ribbon = ribbon

    def _build_half_ribbon(self, refobj, fastobjs):
        found  = True
        ribbon = []

        while found:
            found  = False

            for i, fastobj in enumerate(fastobjs):
                if refobj.is_neighbour(fastobj):
                    found = True
                    ribbon.append(fastobjs.pop(i))
                    refobj = fastobj

        return ribbon

    def __iter__(self):
        return iter(self._ribbon)

    @staticmethod
    def factory(mesh, mode):
        if mode == MODE_FACE:
            return FaceRibbon(mesh)
        elif mode == MODE_EDGE:
            return EdgeRibbon(mesh)
        #mode == MODE_VERT
        return VertexRibbon(mesh)

class FaceRibbon(Ribbon):
    _no_obj_error_msg  = 'You must select at least one face.'
    _no_loop_error_msg = 'You must select a face ribbon/loop.'
    mesh_mode = 'FACE'

    def __init__(self, mesh):
        faces = mesh.faces
        Ribbon.__init__(self, [FastFace(faces[i]) for i in faces.selected()])


class EdgeRibbon(Ribbon):
    _no_obj_error_msg  = 'You must select at least one edge.'
    _no_loop_error_msg = 'You must select an edge ribbon/loop.'
    mesh_mode = 'EDGE'

    def __init__(self, mesh):
        Ribbon.__init__(self, [FastEdge(edge) for edge in mesh.edges if edge.flag & SEL_E])


class VertexRibbon(Ribbon):
    _no_obj_error_msg  = 'You must select at least one vertex.'
    _no_loop_error_msg = 'You must select a vertex ribbon/loop.'
    mesh_mode = 'VERTEX'

    def __init__(self, mesh):
        mesh_verts = mesh.verts
        sel_verts = mesh_verts.selected()
        edict = dict((vidx, set()) for vidx in sel_verts)

        for edge in mesh.edges:
            if edge.flag & SEL_E:
                idx = edge.index
                edict[edge.v1.index].add(idx)
                edict[edge.v2.index].add(idx)

        Ribbon.__init__(self, [FastVert(mesh_verts[i], edict[i]) for i in sel_verts])


################################################################################
#                             FUNCTIONS                                        #
################################################################################

def display_error(string):
    PupMenu("Error !%t|" + string)

def pattern_select(mesh):
    pattern = Pattern()
    gui = GUI(pattern)

    if EVENT_OK != gui.show():
        return

    it = iter(pattern)
    ribbon = Ribbon.factory(mesh, pattern.mode)
    Mesh.Mode(Mesh.SelectModes[ribbon.mesh_mode])

    for elt in ribbon:
        elt.select(it.next())

    pattern.save()


################################################################################
#                           MAIN FUNCTION                                      #
################################################################################

def main():
    """Da main function ! :)"""
    #init
    is_editmode = EditMode()
    if is_editmode:
        EditMode(0)

    try:
        #get selected object (or quit)
        objs = GetSelected()
        if not objs:
            raise PSFatalError("none selected object")

        if len(objs) > 1:
            raise PSFatalError("only one object must be selected")

        obj = objs[0]
        if obj.getType() != "Mesh":
            raise PSFatalError("active object must be a mesh")

        #main treatment
        pattern_select(obj.getData(mesh=True))

    #Exceptions handlers
    except PSFatalError, e:
        print e
        display_error(str(e))
    except PSError, e:
        print e

        if e.priority >= 1:
            display_error(str(e))
    except:
        import sys
        sys.excepthook(*sys.exc_info())
        display_error("An exception occured | (look at the terminal)")

    #finish
    if is_editmode :
        EditMode(1)


################################################################################
#                           MAIN PROGRAM                                       #
################################################################################

main()
