pathscatter.py revision 801127bca2d92350331d75f9c20e4b2e6a280259
1835N/A#!/usr/bin/env python
1835N/A'''
131N/ACopyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
131N/A
1835N/AThis program is free software; you can redistribute it and/or modify
131N/Ait under the terms of the GNU General Public License as published by
131N/Athe Free Software Foundation; either version 2 of the License, or
131N/A(at your option) any later version.
1835N/A
131N/AThis program is distributed in the hope that it will be useful,
131N/Abut WITHOUT ANY WARRANTY; without even the implied warranty of
131N/AMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
131N/AGNU General Public License for more details.
131N/A
131N/AYou should have received a copy of the GNU General Public License
131N/Aalong with this program; if not, write to the Free Software
131N/AFoundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
131N/Abarraud@math.univ-lille1.fr
131N/A
131N/AQuick description:
131N/AThis script deforms an object (the pattern) along other paths (skeletons)...
131N/AThe first selected object is the pattern
131N/Athe last selected ones are the skeletons.
131N/A
131N/AImagine a straight horizontal line L in the middle of the bounding box of the pattern.
131N/AConsider the normal bundle of L: the collection of all the vertical lines meeting L.
131N/AConsider this as the initial state of the plane; in particular, think of the pattern
131N/Aas painted on these lines.
131N/A
131N/ANow move and bend L to make it fit a skeleton, and see what happens to the normals:
131N/Athey move and rotate, deforming the pattern.
131N/A'''
131N/A
131N/Aimport inkex, cubicsuperpath, bezmisc
131N/Aimport pathmodifier, simpletransform
131N/Afrom lxml import etree
131N/A
131N/Aimport copy, math, re, random
131N/A
131N/Adef zSort(inNode,idList):
131N/A sortedList=[]
131N/A theid = inNode.get("id")
131N/A if theid in idList:
131N/A sortedList.append(theid)
131N/A for child in inNode:
131N/A if len(sortedList)==len(idList):
131N/A break
131N/A sortedList+=zSort(child,idList)
131N/A return sortedList
131N/A
131N/A
131N/Adef flipxy(path):
131N/A for pathcomp in path:
131N/A for ctl in pathcomp:
131N/A for pt in ctl:
131N/A tmp=pt[0]
131N/A pt[0]=-pt[1]
1835N/A pt[1]=-tmp
1835N/A
1835N/Adef offset(pathcomp,dx,dy):
131N/A for ctl in pathcomp:
131N/A for pt in ctl:
131N/A pt[0]+=dx
131N/A pt[1]+=dy
131N/A
131N/Adef stretch(pathcomp,xscale,yscale,org):
131N/A for ctl in pathcomp:
131N/A for pt in ctl:
131N/A pt[0]=org[0]+(pt[0]-org[0])*xscale
131N/A pt[1]=org[1]+(pt[1]-org[1])*yscale
131N/A
131N/Adef linearize(p,tolerance=0.001):
131N/A '''
131N/A This function recieves a component of a 'cubicsuperpath' and returns two things:
131N/A The path subdivided in many straight segments, and an array containing the length of each segment.
131N/A
131N/A We could work with bezier path as well, but bezier arc lengths are (re)computed for each point
131N/A in the deformed object. For complex paths, this might take a while.
131N/A '''
131N/A zero=0.000001
131N/A i=0
131N/A d=0
131N/A lengths=[]
131N/A while i<len(p)-1:
131N/A box = bezmisc.pointdistance(p[i ][1],p[i ][2])
131N/A box += bezmisc.pointdistance(p[i ][2],p[i+1][0])
131N/A box += bezmisc.pointdistance(p[i+1][0],p[i+1][1])
131N/A chord = bezmisc.pointdistance(p[i][1], p[i+1][1])
131N/A if (box - chord) > tolerance:
131N/A b1, b2 = bezmisc.beziersplitatt([p[i][1],p[i][2],p[i+1][0],p[i+1][1]], 0.5)
131N/A p[i ][2][0],p[i ][2][1]=b1[1]
131N/A p[i+1][0][0],p[i+1][0][1]=b2[2]
131N/A p.insert(i+1,[[b1[2][0],b1[2][1]],[b1[3][0],b1[3][1]],[b2[1][0],b2[1][1]]])
131N/A else:
131N/A d=(box+chord)/2
131N/A lengths.append(d)
131N/A i+=1
131N/A new=[p[i][1] for i in range(0,len(p)-1) if lengths[i]>zero]
131N/A new.append(p[-1][1])
131N/A lengths=[l for l in lengths if l>zero]
131N/A return(new,lengths)
131N/A
131N/Aclass PathScatter(pathmodifier.Diffeo):
131N/A def __init__(self):
131N/A pathmodifier.Diffeo.__init__(self)
131N/A self.OptionParser.add_option("--title")
131N/A self.OptionParser.add_option("-n", "--noffset",
131N/A action="store", type="float",
131N/A dest="noffset", default=0.0, help="normal offset")
131N/A self.OptionParser.add_option("-t", "--toffset",
131N/A action="store", type="float",
131N/A dest="toffset", default=0.0, help="tangential offset")
131N/A self.OptionParser.add_option("-f", "--follow",
1835N/A action="store", type="inkbool",
1835N/A dest="follow", default=True,
131N/A help="choose between wave or snake effect")
131N/A self.OptionParser.add_option("-s", "--stretch",
131N/A action="store", type="inkbool",
131N/A dest="stretch", default=True,
131N/A help="repeat the path to fit deformer's length")
131N/A self.OptionParser.add_option("-p", "--space",
131N/A action="store", type="float",
131N/A dest="space", default=0.0)
131N/A self.OptionParser.add_option("-v", "--vertical",
131N/A action="store", type="inkbool",
131N/A dest="vertical", default=False,
131N/A help="reference path is vertical")
131N/A self.OptionParser.add_option("-d", "--duplicate",
131N/A action="store", type="inkbool",
131N/A dest="duplicate", default=False,
131N/A help="duplicate pattern before deformation")
131N/A self.OptionParser.add_option("-c", "--copymode",
131N/A action="store", type="string",
131N/A dest="copymode", default="clone",
131N/A help="duplicate pattern before deformation")
131N/A
131N/A def prepareSelectionList(self):
131N/A
131N/A idList=self.options.ids
131N/A idList=zSort(self.document.getroot(),idList)
131N/A
131N/A ##first selected->pattern, all but first selected-> skeletons
131N/A #id = self.options.ids[-1]
131N/A id = idList[-1]
131N/A self.patternNode=self.selected[id]
131N/A
131N/A self.gNode = etree.Element('{http://www.w3.org/2000/svg}g')
131N/A self.patternNode.getparent().append(self.gNode)
131N/A
131N/A if self.options.copymode=="copy":
131N/A duplist=self.duplicateNodes({id:self.patternNode})
131N/A self.patternNode = duplist.values()[0]
131N/A
131N/A #TODO: allow 4th option: duplicate the first copy and clone the next ones.
131N/A if "%s"%self.options.copymode=="clone":
131N/A self.patternNode = etree.Element('{http://www.w3.org/2000/svg}use')
131N/A self.patternNode.set('{http://www.w3.org/1999/xlink}href',"#%s"%id)
131N/A self.gNode.append(self.patternNode)
131N/A
131N/A self.skeletons=self.selected
131N/A del self.skeletons[id]
131N/A self.expandGroupsUnlinkClones(self.skeletons, True, False)
131N/A self.objectsToPaths(self.skeletons,False)
131N/A
131N/A def lengthtotime(self,l):
131N/A '''
131N/A Recieves an arc length l, and returns the index of the segment in self.skelcomp
131N/A containing the coresponding point, to gether with the position of the point on this segment.
131N/A
131N/A If the deformer is closed, do computations modulo the toal length.
131N/A '''
131N/A if self.skelcompIsClosed:
1835N/A l=l % sum(self.lengths)
1835N/A if l<=0:
131N/A return 0,l/self.lengths[0]
131N/A i=0
131N/A while (i<len(self.lengths)) and (self.lengths[i]<=l):
131N/A l-=self.lengths[i]
131N/A i+=1
131N/A t=l/self.lengths[min(i,len(self.lengths)-1)]
131N/A return i, t
131N/A
131N/A def localTransformAt(self,s,follow=True):
131N/A '''
131N/A recieves a length, and returns the coresponding point and tangent of self.skelcomp
131N/A if follow is set to false, returns only the translation
131N/A '''
131N/A i,t=self.lengthtotime(s)
131N/A if i==len(self.skelcomp)-1:
131N/A x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
131N/A dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
131N/A dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
131N/A else:
131N/A x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
131N/A dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
131N/A dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
131N/A if follow:
131N/A mat=[[dx,-dy,x],[dy,dx,y]]
131N/A else:
131N/A mat=[[1,0,x],[0,1,y]]
131N/A return mat
131N/A
131N/A
131N/A def effect(self):
131N/A
131N/A if len(self.options.ids)<2:
131N/A inkex.debug("This extension requires that you select two paths.")
131N/A return
131N/A self.prepareSelectionList()
131N/A
131N/A #center at (0,0)
131N/A bbox=pathmodifier.computeBBox([self.patternNode])
131N/A mat=[[1,0,-(bbox[0]+bbox[1])/2],[0,1,-(bbox[2]+bbox[3])/2]]
131N/A if self.options.vertical:
131N/A bbox=[-bbox[3],-bbox[2],bbox[0],bbox[1]]
131N/A mat=simpletransform.composeTransform([[0,-1,0],[1,0,0]],mat)
131N/A mat[1][2] += self.options.noffset
131N/A simpletransform.applyTransformToNode(mat,self.patternNode)
131N/A
131N/A width=bbox[1]-bbox[0]
131N/A dx=width+self.options.space
131N/A
131N/A for skelnode in self.skeletons.itervalues():
131N/A self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
131N/A for comp in self.curSekeleton:
131N/A self.skelcomp,self.lengths=linearize(comp)
131N/A #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
131N/A self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
131N/A
131N/A length=sum(self.lengths)
1835N/A if self.options.stretch:
1835N/A dx=width+self.options.space
131N/A n=int((length-self.options.toffset+self.options.space)/dx)
131N/A if n>0:
131N/A dx=(length-self.options.toffset)/n
131N/A
131N/A
131N/A xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
131N/A yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
131N/A
131N/A s=self.options.toffset
131N/A while s<=length:
131N/A mat=self.localTransformAt(s,self.options.follow)
131N/A
131N/A clone=copy.deepcopy(self.patternNode)
131N/A #!!!--> should it be given an id?
131N/A #seems to work without this!?!
131N/A myid = self.patternNode.tag.split('}')[-1]
131N/A clone.set("id", self.uniqueId(myid))
131N/A self.gNode.append(clone)
131N/A
131N/A simpletransform.applyTransformToNode(mat,clone)
131N/A
131N/A s+=dx
131N/A self.patternNode.getparent().remove(self.patternNode)
131N/A
131N/A
131N/Ae = PathScatter()
131N/Ae.affect()
131N/A
131N/A
1835N/A