Code

Extensions. Shebangs branch merge.
[inkscape.git] / share / extensions / pathalongpath.py
1 #!/usr/bin/env python
2 '''
3 Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 barraud@math.univ-lille1.fr
20 Quick description:
21 This script deforms an object (the pattern) along other paths (skeletons)...
22 The first selected object is the pattern
23 the last selected ones are the skeletons.
25 Imagine a straight horizontal line L in the middle of the bounding box of the pattern.
26 Consider the normal bundle of L: the collection of all the vertical lines meeting L.
27 Consider this as the initial state of the plane; in particular, think of the pattern
28 as painted on these lines.
30 Now move and bend L to make it fit a skeleton, and see what happens to the normals:
31 they move and rotate, deforming the pattern.
32 '''
34 import inkex, cubicsuperpath, bezmisc
35 import pathmodifier,simpletransform
36 import copy, math, re, random
37 import gettext
38 _ = gettext.gettext
40 def flipxy(path):
41     for pathcomp in path:
42         for ctl in pathcomp:
43             for pt in ctl:
44                 tmp=pt[0]
45                 pt[0]=-pt[1]
46                 pt[1]=-tmp
48 def offset(pathcomp,dx,dy):
49     for ctl in pathcomp:
50         for pt in ctl:
51             pt[0]+=dx
52             pt[1]+=dy
54 def stretch(pathcomp,xscale,yscale,org):
55     for ctl in pathcomp:
56         for pt in ctl:
57             pt[0]=org[0]+(pt[0]-org[0])*xscale
58             pt[1]=org[1]+(pt[1]-org[1])*yscale
60 def linearize(p,tolerance=0.001):
61     '''
62     This function recieves a component of a 'cubicsuperpath' and returns two things:
63     The path subdivided in many straight segments, and an array containing the length of each segment.
64     
65     We could work with bezier path as well, but bezier arc lengths are (re)computed for each point 
66     in the deformed object. For complex paths, this might take a while.
67     '''
68     zero=0.000001
69     i=0
70     d=0
71     lengths=[]
72     while i<len(p)-1:
73         box  = bezmisc.pointdistance(p[i  ][1],p[i  ][2])
74         box += bezmisc.pointdistance(p[i  ][2],p[i+1][0])
75         box += bezmisc.pointdistance(p[i+1][0],p[i+1][1])
76         chord = bezmisc.pointdistance(p[i][1], p[i+1][1])
77         if (box - chord) > tolerance:
78             b1, b2 = bezmisc.beziersplitatt([p[i][1],p[i][2],p[i+1][0],p[i+1][1]], 0.5)
79             p[i  ][2][0],p[i  ][2][1]=b1[1]
80             p[i+1][0][0],p[i+1][0][1]=b2[2]
81             p.insert(i+1,[[b1[2][0],b1[2][1]],[b1[3][0],b1[3][1]],[b2[1][0],b2[1][1]]])
82         else:
83             d=(box+chord)/2
84             lengths.append(d)
85             i+=1
86     new=[p[i][1] for i in range(0,len(p)-1) if lengths[i]>zero]
87     new.append(p[-1][1])
88     lengths=[l for l in lengths if l>zero]
89     return(new,lengths)
91 class PathAlongPath(pathmodifier.Diffeo):
92     def __init__(self):
93         pathmodifier.Diffeo.__init__(self)
94         self.OptionParser.add_option("--title")
95         self.OptionParser.add_option("-n", "--noffset",
96                         action="store", type="float", 
97                         dest="noffset", default=0.0, help="normal offset")
98         self.OptionParser.add_option("-t", "--toffset",
99                         action="store", type="float", 
100                         dest="toffset", default=0.0, help="tangential offset")
101         self.OptionParser.add_option("-k", "--kind",
102                         action="store", type="string", 
103                         dest="kind", default=True,
104                         help="choose between wave or snake effect")
105         self.OptionParser.add_option("-c", "--copymode",
106                         action="store", type="string", 
107                         dest="copymode", default=True,
108                         help="repeat the path to fit deformer's length")
109         self.OptionParser.add_option("-p", "--space",
110                         action="store", type="float", 
111                         dest="space", default=0.0)
112         self.OptionParser.add_option("-v", "--vertical",
113                         action="store", type="inkbool", 
114                         dest="vertical", default=False,
115                         help="reference path is vertical")
116         self.OptionParser.add_option("-d", "--duplicate",
117                         action="store", type="inkbool", 
118                         dest="duplicate", default=False,
119                         help="duplicate pattern before deformation")
121     def prepareSelectionList(self):
123         idList=self.options.ids
124         idList=pathmodifier.zSort(self.document.getroot(),idList)
125         id = idList[-1]
126         self.patterns={id:self.selected[id]}
128 ##        ##first selected->pattern, all but first selected-> skeletons
129 ##        id = self.options.ids[-1]
130 ##        self.patterns={id:self.selected[id]}
132         if self.options.duplicate:
133             self.patterns=self.duplicateNodes(self.patterns)
134         self.expandGroupsUnlinkClones(self.patterns, True, True)
135         self.objectsToPaths(self.patterns)
136         del self.selected[id]
138         self.skeletons=self.selected
139         self.expandGroupsUnlinkClones(self.skeletons, True, False)
140         self.objectsToPaths(self.skeletons)
142     def lengthtotime(self,l):
143         '''
144         Recieves an arc length l, and returns the index of the segment in self.skelcomp 
145         containing the coresponding point, to gether with the position of the point on this segment.
147         If the deformer is closed, do computations modulo the toal length.
148         '''
149         if self.skelcompIsClosed:
150             l=l % sum(self.lengths)
151         if l<=0:
152             return 0,l/self.lengths[0]
153         i=0
154         while (i<len(self.lengths)) and (self.lengths[i]<=l):
155             l-=self.lengths[i]
156             i+=1
157         t=l/self.lengths[min(i,len(self.lengths)-1)]
158         return i, t
160     def applyDiffeo(self,bpt,vects=()):
161         '''
162         The kernel of this stuff:
163         bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
164         '''
165         s=bpt[0]-self.skelcomp[0][0]
166         i,t=self.lengthtotime(s)
167         if i==len(self.skelcomp)-1:
168             x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
169             dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
170             dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
171         else:
172             x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
173             dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
174             dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
176         vx=0
177         vy=bpt[1]-self.skelcomp[0][1]
178         if self.options.wave:
179             bpt[0]=x+vx*dx
180             bpt[1]=y+vy+vx*dy
181         else:
182             bpt[0]=x+vx*dx-vy*dy
183             bpt[1]=y+vx*dy+vy*dx
185         for v in vects:
186             vx=v[0]-self.skelcomp[0][0]-s
187             vy=v[1]-self.skelcomp[0][1]
188             if self.options.wave:
189                 v[0]=x+vx*dx
190                 v[1]=y+vy+vx*dy
191             else:
192                 v[0]=x+vx*dx-vy*dy
193                 v[1]=y+vx*dy+vy*dx
195     def effect(self):
196         if len(self.options.ids)<2:
197             inkex.errormsg(_("This extension requires two selected paths."))
198             return
199         self.prepareSelectionList()
200         self.options.wave = (self.options.kind=="Ribbon")
201         if self.options.copymode=="Single":
202             self.options.repeat =False
203             self.options.stretch=False
204         elif self.options.copymode=="Repeated":
205             self.options.repeat =True
206             self.options.stretch=False
207         elif self.options.copymode=="Single, stretched":
208             self.options.repeat =False
209             self.options.stretch=True
210         elif self.options.copymode=="Repeated, stretched":
211             self.options.repeat =True
212             self.options.stretch=True
214         bbox=simpletransform.computeBBox(self.patterns.values())
215                     
216         if self.options.vertical:
217             #flipxy(bbox)...
218             bbox=(-bbox[3],-bbox[2],-bbox[1],-bbox[0])
220         width=bbox[1]-bbox[0]
221         dx=width+self.options.space
222         if dx < 0.01:
223             exit(_("The total length of the pattern is too small :\nPlease choose a larger object or set 'Space between copies' > 0"))
225         for id, node in self.patterns.iteritems():
226             if node.tag == inkex.addNS('path','svg') or node.tag=='path':
227                 d = node.get('d')
228                 p0 = cubicsuperpath.parsePath(d)
229                 if self.options.vertical:
230                     flipxy(p0)
232                 newp=[]
233                 for skelnode in self.skeletons.itervalues(): 
234                     self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
235                     if self.options.vertical:
236                         flipxy(self.curSekeleton)
237                     for comp in self.curSekeleton:
238                         p=copy.deepcopy(p0)
239                         self.skelcomp,self.lengths=linearize(comp)
240                         #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
241                         self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
243                         length=sum(self.lengths)
244                         xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
245                         yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
248                         if self.options.repeat:
249                             NbCopies=max(1,int(round((length+self.options.space)/dx)))
250                             width=dx*NbCopies
251                             if not self.skelcompIsClosed:
252                                 width-=self.options.space
253                             bbox=bbox[0],bbox[0]+width, bbox[2],bbox[3]
254                             new=[]
255                             for sub in p:
256                                 for i in range(0,NbCopies,1):
257                                     new.append(copy.deepcopy(sub))
258                                     offset(sub,dx,0)
259                             p=new
261                         for sub in p:
262                             offset(sub,xoffset,yoffset)
264                         if self.options.stretch:
265                             for sub in p:
266                                 stretch(sub,length/width,1,self.skelcomp[0])
268                         for sub in p:
269                             for ctlpt in sub:
270                                 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))
272                         if self.options.vertical:
273                             flipxy(p)
274                         newp+=p
276                 node.set('d', cubicsuperpath.formatPath(newp))
278 if __name__ == '__main__':
279     e = PathAlongPath()
280     e.affect()
282                     
283 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99