2.5: Top Menu
[blender.git] / release / scripts / mesh_unfolder.py
index f725da318374aaebd39a8c9f339474d8a5176912..f5c19a92bd027af76c3acacf9cae28a324c01c61 100644 (file)
@@ -1,10 +1,10 @@
 #!BPY
 """
 Name: 'Unfold'
-Blender: 243
+Blender: 245
 Group: 'Mesh'
 Tip: 'Unfold meshes to create nets'
-Version:  v2.2.3
+Version:  v2.5
 Author: Matthew Chadwick
 """
 import Blender
@@ -18,16 +18,18 @@ try:
        from math import *
        import sys
        import random
-       from decimal import *
        import xml.sax, xml.sax.handler, xml.sax.saxutils
+       
+       # annoying but need so classes dont raise errors
+       xml_sax_handler_ContentHandler = xml.sax.handler.ContentHandler
 
 except:
-       print "One of the Python modules required can't be found."
-       print sys.exc_info()[1]
-       traceback.print_exc(file=sys.stdout)
-       
+       Draw.PupMenu('Error%t|A full python installation is required to run this script.')
+       xml = None
+       xml_sax_handler_ContentHandler = type(0)
+
 __author__ = 'Matthew Chadwick'
-__version__ = '2.2.3 07032007'
+__version__ = '2.5 06102007'
 __url__ = ["http://celeriac.net/unfolder/", "blender", "blenderartist"]
 __email__ = ["post at cele[remove this text]riac.net", "scripts"]
 __bpydoc__ = """\
@@ -47,10 +49,27 @@ of the GNU General Public License as published by the Free Software Foundation;
 or later, currently at http://www.gnu.org/copyleft/gpl.html
 
 The idea came while I was riding a bike.
-
 """    
-       
 
+# ***** 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 *****
+
+# Face lookup
 class FacesAndEdges:
        def __init__(self, mesh):
                self.nfaces = 0
@@ -62,7 +81,7 @@ class FacesAndEdges:
                                        self.edgeFaces[key].append(face)
        def findTakenAdjacentFace(self, bface, edge):
                return self.findAdjacentFace(bface, edge)
-       # find the first untaken (non-selected) adjacent face in the list of adjacent faces for the given edge
+       # find the first untaken (non-selected) adjacent face in the list of adjacent faces for the given edge (allows for manifold meshes too)
        def findAdjacentFace(self, bface, edge):
                faces = self.edgeFaces[edge.key()]
                for i in xrange(len(faces)):
@@ -89,90 +108,10 @@ class FacesAndEdges:
        def takeFace(self, bface):
                if(bface!=None):
                        bface.sel= True
-                       self.nfaces+=1          
-       def minz(mesh):
-               return min([v.co.z for v in mesh.verts]) 
-       minz = staticmethod(minz)
-       
-       
-class IntersectionResult:
-       def __init__(self, rn, rd, v=None):
-               self.v = v
-               self.rd = rd
-               self.rn = rn
-       def intersected(self):
-               return not(not(self.v))
-       def isParallel(self):
-               return (self.rd==0)
-       def isColinear(self):
-               return (self.rn==0)
-       def intersection(self):
-               return self.v
-       
-# represents a line segment between two points [p1, p2]. the points are [x,y]
-class LineSegment:
-       def __init__(self, p):
-               self.p = p
-       def intersects(self, s):
-               rn = ((self.p[0].y-s.p[0].y)*(s.p[1].x-s.p[0].x)-(self.p[0].x-s.p[0].x)*(s.p[1].y-s.p[0].y)) 
-               rd = ((self.p[1].x-self.p[0].x)*(s.p[1].y-s.p[0].y)-(self.p[1].y-self.p[0].y)*(s.p[1].x-s.p[0].x))
-               # need an epsilon closeTo() here
-               if(rd<0.0000001 or rn==0.0):
-                       return IntersectionResult(rn,rd)
-               r = rn/rd
-               s = ((self.p[0].y-s.p[0].y)*(self.p[1].x-self.p[0].x)-(self.p[0].x-s.p[0].x)*(self.p[1].y-self.p[0].y)) / rd
-               i = (0.0<=r and r<=1.0 and 0.0<=s and s<=1.0)
-               if not(i):
-                       return None
-               ix = self.p[0].x + r*(self.p[1].x - self.p[0].x)
-               iy = self.p[0].y + r*(self.p[1].y - self.p[0].y)
-               t = 0.0001
-               if ( abs(ix-self.p[0].x)>t and abs(iy-self.p[0].x)>t and abs(ix-self.p[1].x)>t and abs(iy-self.p[1].y)>t ):
-                       return IntersectionResult( rn, rd,Vector([ix,iy,0.0]))
-               else:
-                       return None
-               
-class LineSegments:
-       def __init__(self, face):
-               self.face = face
-       def segmentAt(self, i):
-               if(i>self.face.nPoints()-1):
-                       return None
-               if(i==self.face.nPoints()-1):
-                       j = 0
-               else:
-                       j = i+1
-               return LineSegment([ self.face.v[i], self.face.v[j] ])
-       def iterateSegments(self, something):
-               results = []
-               for i in xrange(self.face.nPoints()):
-                       results.extend(something.haveSegment(self.segmentAt(i))) 
-               return results
-       def compareSegments(self, something, segment):
-               results = []
-               for i in xrange(self.face.nPoints()):
-                       results.append(something.compareSegments([self.segmentAt(i), segment]))
-               return results
-
-class FaceOverlapTest:
-       def __init__(self, face1, face2):
-               self.faces = [face1, face2]
-               self.segments = [ LineSegments(self.faces[0]), LineSegments(self.faces[1]) ]
-       def suspectsOverlap(self):
-               tests = self.segments[0].iterateSegments(self)
-               gi = 0
-               for i in tests:
-                       if( i!=None and i.intersected() ):
-                               gi+=1
-               return gi>0
-       def haveSegment(self, segment):
-               return self.segments[1].compareSegments(self, segment)
-       def compareSegments(self, segments):
-               return segments[0].intersects(segments[1])
+                       self.nfaces+=1
 
-               
        
-# A fold
+# A fold between two faces with a common edge
 class Fold:
        ids = -1
        def __init__(self, parent, refPoly, poly, edge, angle=None):
@@ -225,8 +164,8 @@ class Fold:
                sangle = Mathutils.AngleBetweenVecs(self.refPolyNormal, self.polyNormal)
                if(sangle!=sangle):
                        sangle=0.0
-               ncp = Mathutils.CrossVecs(self.refPolyNormal, self.polyNormal)
-               dp = Mathutils.DotVecs(ncp, self.edge.vector)
+               ncp = self.refPolyNormal.cross(self.polyNormal)
+               dp = ncp.dot(self.edge.vector)
                if(dp>0.0):
                        return +sangle
                else:
@@ -254,7 +193,7 @@ class Fold:
 class Cut(Fold):
        pass
                        
-# Builds folds
+# Trees build folds by traversing the mesh according to a local measure
 class Tree:
        def __init__(self, net, parent,fold,otherConstructor=None):
                self.net = net
@@ -330,7 +269,7 @@ class Tree:
                if(self.net.diffuse==False):
                        fold.tree.grow()
 
-# Nets
+# A Net is the result of the traversal of the mesh by Trees
 class Net:
        def __init__(self, src, des):
                self.src = src
@@ -371,7 +310,7 @@ class Net:
                self.showProgress = show
        # this method really needs work
        def unfold(self):
-               selectedFaces = [face for face in self.src.faces if (self.src.faceUV and face.flag & Mesh.FaceFlags.SELECT)]
+               selectedFaces = [face for face in self.src.faces if (self.src.faceUV and face.sel)]
                if(self.avoidsOverlaps):
                        print "unfolding with overlap detection"
                if(self.firstFaceIndex==None):
@@ -379,12 +318,12 @@ class Net:
                else:
                        print "Using user-selected seed face ", self.firstFaceIndex
                self.firstFace = self.src.faces[self.firstFaceIndex]
-               z = FacesAndEdges.minz(self.src)-0.1
+               z = min([v.co.z for v in self.src.verts])-0.1
                ff = Poly.fromBlenderFace(self.firstFace)
                if(len(ff.v)<3):
-                       raise Exception("This mesh contains an isolated edge - it must consist of only faces")
+                       raise Exception("This mesh contains an isolated edge - it must consist only of faces")
                testFace = Poly.fromVectors( [ Vector([0.0,0.0,0.0]), Vector([0.0,1.0,0.0]), Vector([1.0,1.0,0.0])  ] )
-               # hmmm
+               # hmmm. I honestly can't remember why this needs to be done, but it does.
                u=0
                v=1
                w=2
@@ -392,23 +331,25 @@ class Net:
                        u=1
                        v=2
                        w=0
+               # here we make a couple of folds, not part of the net, which serve to get the net into the xy plane
                xyFace = Poly.fromList( [ [ff.v[u].x,ff.v[u].y, z] , [ff.v[v].x,ff.v[v].y, z] , [ff.v[w].x+0.1,ff.v[w].y+0.1, z] ] )
                refFace = Poly.fromVectors([ ff.v[u], ff.v[v], xyFace.v[1], xyFace.v[0] ] )
                xyFold =  Fold(None,   xyFace, refFace, Edge(xyFace.v[0], xyFace.v[1] ))
                self.refFold = Fold(xyFold, refFace, ff,         Edge(refFace.v[0], refFace.v[1] ))
                self.refFold.srcFace = self.firstFace
+               # prepare to grow the trees
                trunk = Tree(self, None, self.refFold)
                trunk.generations = self.generations
                self.firstPoly = ff
                self.facesAndEdges.takeFace(self.firstFace)
                self.myFacesVisited+=1
                self.refFold.unfold()
-               # All of his geese are swans
                self.refFold.tree = trunk
                self.refFold.desFace = self.addFace(self.refFold.unfoldedFace(), self.refFold.srcFace)
                self.folds.append(self.refFold)
                trunk.grow()
                i = 0
+               # keep the trees growing while they can
                while(self.myFacesVisited<len(self.src.faces) and len(self.branches) > 0):
                        if self.edgeIteratorClass==RandomEdgeIterator:
                                i = random.randint(0,len(self.branches)-1)
@@ -422,13 +363,11 @@ class Net:
                                        i = 0
                                else:
                                        i = (i + 1) % len(self.branches)
-               try:
+               if self.src.faceUV:
                        for face in self.src.faces:
-                               face.flag = 0   
+                               face.sel = False
                        for face in selectedFaces:
-                               face.flag = Mesh.FaceFlags.SELECT
-               except:
-                       pass
+                               face.sel = True
                self.src.update()
                Window.RedrawAll()
        def assignUVs(self):
@@ -447,11 +386,12 @@ class Net:
                for afold in folds:
                        mdf = afold.unfoldedFace()
                        if(afold!=fold):
-                               it1 = FaceOverlapTest(mf, mdf)
-                               it2 = FaceOverlapTest(mdf, mf)
-                               overlap = (it1.suspectsOverlap() or it2.suspectsOverlap())
+                               # currently need to get agreement from both polys because
+                               # a touch by a vertex of one the other's edge is acceptable &
+                               # they disagree on that
+                               intersects = mf.intersects2D(mdf) and mdf.intersects2D(mf)
                                inside = ( mdf.containsAnyOf(mf) or mf.containsAnyOf(mdf) )
-                               if(  overlap or inside or mdf.overlays(mf)):
+                               if(  intersects or inside or mdf.overlays(mf)):
                                        c.append(afold)
                return c
        def getOverlapsBetweenGL(self, fold, folds):
@@ -632,9 +572,9 @@ class Net:
                        overlaps = self.report()
                        attempts+=1
                return attempts
-       def unfoldSelected(feedback=None, netName=None):
+       def fromSelected(feedback=None, netName=None):
                return Net.createNet(Blender.Object.GetSelected()[0], feedback, netName)
-       unfoldSelected = staticmethod(unfoldSelected)
+       fromSelected = staticmethod(fromSelected)
        def clone(self, object=None):
                if(object==None):
                        object = self.object
@@ -667,7 +607,7 @@ class Net:
                except:
                        print "Problem setting materials here"
                net = Net(mesh, netMesh)
-               if(mesh.faceUV and mesh.activeFace>=0 and (mesh.faces[mesh.activeFace].flag & Mesh.FaceFlags.SELECT)):
+               if mesh.faceUV and mesh.activeFace>=0 and (mesh.faces[mesh.activeFace].sel):
                        net.firstFaceIndex = mesh.activeFace
                net.object = ob
                net.feedback = feedback
@@ -694,7 +634,7 @@ class Net:
        def getSourceMesh(self):
                return self.src
                
-               
+# determines the order in which to visit faces according to a local measure            
 class EdgeIterator:
        def __init__(self, branch, otherConstructor=None):
                self.branch = branch
@@ -759,16 +699,18 @@ class Brightest(EdgeIterator):
                        f = self.net.facesAndEdges.findAdjacentFace(self.bface, edge)
                        if(f!=None):
                                b = 0
-                               for c in f.col:
-                                       b+=(c.g+c.r+c.b)
+                               if self.net.src.vertexColors:
+                                       for c in f.col:
+                                               b+=(c.g+c.r+c.b)
                                rc = float(random.randint(0, self.net.srcSize())) / float(self.net.srcSize()) / 100.0
                                b+=rc
                                edge.setGoodness(b)
                self.edges.sort(lambda e1, e2: e1.compare(e2))
        def computeGoodness(self):
                g = 0
-               for c in self.bface.col:
-                       g+=(c.g+c.r+c.b)
+               if self.net.src.vertexColors:
+                       for c in self.bface.col:
+                               g+=(c.g+c.r+c.b)
                self.gooodness = g
                
 class OddEven(EdgeIterator):
@@ -778,7 +720,6 @@ class OddEven(EdgeIterator):
                if(OddEven.i):
                        self.edges.reverse()
                
-# local curvature
 class Curvature(EdgeIterator):
        def sequenceEdges(self):
                p1 = Poly.fromBlenderFace(self.bface)
@@ -810,11 +751,11 @@ class Curvature(EdgeIterator):
                                g += f.dihedralAngle()
                self.gooodness = g
                
-       
+
 class Edge:
        def __init__(self, v1=None, v2=None, mEdge=None, i=-1):
                self.idx = i
-               if v1 and v2: # Neither are None
+               if v1 and v2:
                        self.v1 = v1.copy()
                        self.v2 = v2.copy()
                else:
@@ -854,7 +795,6 @@ class Edge:
                return e
        edgesOfBlenderFace=staticmethod(edgesOfBlenderFace)
        def isBlenderSeam(self):
-               # Better and flutter must and man can beam. Now think of seams.
                return (self.bmEdge.flag & Mesh.EdgeFlags.SEAM)
        def isInFGon(self):
                return (self.bmEdge.flag & Mesh.EdgeFlags.FGON)
@@ -879,6 +819,23 @@ class Edge:
                        return +1
                else:
                        return -1
+       # Does the given segment intersect this, for overlap detection.
+       # endpoints are allowed to touch the line segment
+       def intersects2D(self, s):
+               if(self.matches(s)):
+                       return False
+               else:
+                       i = Geometry.LineIntersect2D(self.v1, self.v2, s.v1, s.v2)
+                       if(i!=None):
+                               i.resize4D()
+                               i.z = self.v1.z # hack to put the point on the same plane as this edge for comparison
+                       return(i!=None and not(self.endsWith(i)))
+       def matches(self, s):
+               return ( (self.v1==s.v1 and self.v2==s.v2) or (self.v2==s.v1 and self.v1==s.v2) )
+       # Is the given point on the end of this segment ? 10-5 seems to an acceptable limit for closeness in Blender
+       def endsWith(self, aPoint, e=0.0001):
+               return ( (self.v1-aPoint).length < e or (self.v2-aPoint).length < e )
+
        
 class Poly:
        ids = -1
@@ -887,6 +844,7 @@ class Poly:
                self.v = []
                self.id = Poly.ids
                self.boundz = None
+               self.edges = None
        def getID(self):
                return self.id
        def normal(self):
@@ -897,7 +855,22 @@ class Poly:
                p.resize3D()
                q = a-c
                q.resize3D()
-               return CrossVecs(p,q)
+               return p.cross(q)
+       def makeEdges(self):
+               self.edges = []
+               for i in xrange(self.nPoints()):
+                       self.edges.append(Edge( self.v[i % self.nPoints()], self.v[ (i+1) % self.nPoints()] ))
+       def edgeAt(self, i):
+               if(self.edges==None):
+                       self.makeEdges()
+               return self.edges[i]
+       def intersects2D(self, poly):
+               for i in xrange(self.nPoints()):
+                       edge = self.edgeAt(i)
+                       for j in xrange(poly.nPoints()):
+                               if edge.intersects2D(poly.edgeAt(j)):
+                                       return True
+               return False
        def isBad(self):
                badness = 0
                for vv in self.v:
@@ -1019,8 +992,7 @@ class Poly:
        def toString(self):
                return self.v
        # This is the BEST algorithm for point-in-polygon detection.
-       # It's by W. Randolph Franklin. It's also very beautiful (looks even better in C).
-       # All the others are shite; they give false positives.
+       # It's by W. Randolph Franklin.
        # returns 1 for inside, 1 or 0 for edges
        def contains(self, tp):
                c = 0
@@ -1102,6 +1074,7 @@ class SVGExporter:
        def export(self):
                self.net.unfoldTo(1)
                bb = self.object.getBoundBox()
+               print bb
                self.vxmin = bb[0][0]
                self.vymin = bb[0][1]
                self.vxmax = bb[7][0]
@@ -1129,14 +1102,14 @@ class SVGExporter:
                self.addPolys()
                self.e.endElement("clipPath")
        def addUVImage(self):
-               image = Blender.Image.GetCurrent()
+               image = Blender.Image.GetCurrent() #hmm - how to determine the desired image ?
                if image==None:
                        return
                ifn = image.getFilename()
-               #ifn = self.filename.replace(".svg", ".jpg")
-               #image.setFilename(ifn)
-               #ifn = ifn[ifn.rfind("/")+1:]
-               #image.save()
+               ifn = self.filename.replace(".svg", ".jpg")
+               image.setFilename(ifn)
+               ifn = ifn[ifn.rfind("/")+1:]
+               image.save()
                atts = {}
                atts["clip-path"] = "url(#netClip)"
                atts["xlink:href"] = ifn
@@ -1212,7 +1185,7 @@ class SVGExporter:
                for vv in face.v:
                        if(not(first)):
                                points+=','
-                       first = (2==3)
+                       first = False
                        points+=str(vv[0])
                        points+=' '
                        points+=str(vv[1])
@@ -1230,7 +1203,7 @@ class SVGExporter:
                        traceback.print_exc(file=sys.stdout)
        fileSelected = staticmethod(fileSelected)       
 
-
+# for importing nets saved by the above exporter
 class NetHandler(xml.sax.handler.ContentHandler):
        def __init__(self, net):
                self.net = net
@@ -1336,10 +1309,7 @@ class NetHandler(xml.sax.handler.ContentHandler):
                        traceback.print_exc(file=sys.stdout)
        fileSelected = staticmethod(fileSelected)               
 
-       
-       
-#____________Blender GUI__________________
-       
+
 class GUI:
        def __init__(self):
                self.overlaps = Draw.Create(0)
@@ -1404,7 +1374,7 @@ class GUI:
                        while(s):# and search < searchLimit):
                                if(net!=None):
                                        name = net.des.name
-                               net = Net.unfoldSelected(self, name)
+                               net = Net.fromSelected(self, name)
                                net.setAvoidsOverlaps(not(self.overlaps.val))
                                print
                                print "Unfolding selected object"
@@ -1507,6 +1477,14 @@ class GUI:
                        else:
                                self.nOverlaps = 0
                        Draw.Redraw(1)
+               if(evt==233):
+                       f1 = Poly.fromBlenderFace(Blender.Object.GetSelected()[0].getData().faces[0])
+                       f2 = Poly.fromBlenderFace(Blender.Object.GetSelected()[1].getData().faces[0])
+                       print
+                       print Blender.Object.GetSelected()[0].getName()
+                       print Blender.Object.GetSelected()[1].getName()
+                       print f1.intersects2D(f2)
+                       print f2.intersects2D(f1)
                if(evt==714):
                        Net.unfoldAll(self)
                        Draw.Redraw(1)
@@ -1558,6 +1536,7 @@ class GUI:
                Draw.Button("Unfold",           1, l.nx(), l.ny(), l.cw, l.ch, "Unfold selected mesh to net")
                Draw.Button("save",             104,   l.nx(), l.ny(), l.cw, l.ch,  "Save net as SVG")
                Draw.Button("load",             107,   l.nx(), l.ny(), l.cw, l.ch,  "Load net from SVG")
+               #Draw.Button("test",             233,   l.nx(), l.ny(), l.cw, l.ch,  "test")
                # unfolding enthusiasts - try uncommenting this
                self.ancestors = Draw.Number("depth", 654,        l.nx(), l.ny(), cw, ch, self.ancestors.val, 0, 9999,  "depth of branching 0=diffuse")
                #self.noise = Draw.Number("noise", 631,        l.nx(), l.ny(), cw, ch, self.noise.val, 0.0, 1.0,  "noisyness of branching")
@@ -1565,7 +1544,7 @@ class GUI:
                options = "order %t|random %x0|brightest %x1|curvature %x2|winding %x3| 1010 %x4|largest %x5"
                self.shape = Draw.Menu(options, 713,  l.nx(), l.ny(), cw, ch, self.shape.val, "shape of net")
                Draw.Button("exit",         6,   l.nx(), l.ny(), l.cw, l.ch, "exit")
-               BGL.glClearColor(0.5, 0.5, 0.5, 1)
+               BGL.glClearColor(0.3, 0.3, 0.3, 1)
                BGL.glColor3f(0.3,0.3,0.3)
                l.newLine()
                BGL.glRasterPos2i(32, 100)
@@ -1592,10 +1571,12 @@ class FlowLayout:
                self.y-=self.ch+self.margin
                self.x = self.margin
 
-try:
-       sys.setrecursionlimit(10000)
-       gui = GUI()
-       gui.makeStandardGUI()
-       #gui.makePopupGUI()
-except:
-       pass
+# if xml is None, then dont bother running the script
+if xml:
+       try:
+               sys.setrecursionlimit(10000)
+               gui = GUI()
+               gui.makeStandardGUI()
+               #gui.makePopupGUI()
+       except:
+               traceback.print_exc(file=sys.stdout)