Code

Translations. POTFILES.in cleanup and inkscape.pot update.
[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")
120         self.OptionParser.add_option("--tab",
121                         action="store", type="string",
122                         dest="tab",
123                         help="The selected UI-tab when OK was pressed")
125     def prepareSelectionList(self):
127         idList=self.options.ids
128         idList=pathmodifier.zSort(self.document.getroot(),idList)
129         id = idList[-1]
130         self.patterns={id:self.selected[id]}
132 ##        ##first selected->pattern, all but first selected-> skeletons
133 ##        id = self.options.ids[-1]
134 ##        self.patterns={id:self.selected[id]}
136         if self.options.duplicate:
137             self.patterns=self.duplicateNodes(self.patterns)
138         self.expandGroupsUnlinkClones(self.patterns, True, True)
139         self.objectsToPaths(self.patterns)
140         del self.selected[id]
142         self.skeletons=self.selected
143         self.expandGroupsUnlinkClones(self.skeletons, True, False)
144         self.objectsToPaths(self.skeletons)
146     def lengthtotime(self,l):
147         '''
148         Recieves an arc length l, and returns the index of the segment in self.skelcomp 
149         containing the coresponding point, to gether with the position of the point on this segment.
151         If the deformer is closed, do computations modulo the toal length.
152         '''
153         if self.skelcompIsClosed:
154             l=l % sum(self.lengths)
155         if l<=0:
156             return 0,l/self.lengths[0]
157         i=0
158         while (i<len(self.lengths)) and (self.lengths[i]<=l):
159             l-=self.lengths[i]
160             i+=1
161         t=l/self.lengths[min(i,len(self.lengths)-1)]
162         return i, t
164     def applyDiffeo(self,bpt,vects=()):
165         '''
166         The kernel of this stuff:
167         bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
168         '''
169         s=bpt[0]-self.skelcomp[0][0]
170         i,t=self.lengthtotime(s)
171         if i==len(self.skelcomp)-1:
172             x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
173             dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
174             dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
175         else:
176             x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
177             dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
178             dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
180         vx=0
181         vy=bpt[1]-self.skelcomp[0][1]
182         if self.options.wave:
183             bpt[0]=x+vx*dx
184             bpt[1]=y+vy+vx*dy
185         else:
186             bpt[0]=x+vx*dx-vy*dy
187             bpt[1]=y+vx*dy+vy*dx
189         for v in vects:
190             vx=v[0]-self.skelcomp[0][0]-s
191             vy=v[1]-self.skelcomp[0][1]
192             if self.options.wave:
193                 v[0]=x+vx*dx
194                 v[1]=y+vy+vx*dy
195             else:
196                 v[0]=x+vx*dx-vy*dy
197                 v[1]=y+vx*dy+vy*dx
199     def effect(self):
200         if len(self.options.ids)<2:
201             inkex.errormsg(_("This extension requires two selected paths."))
202             return
203         self.prepareSelectionList()
204         self.options.wave = (self.options.kind=="Ribbon")
205         if self.options.copymode=="Single":
206             self.options.repeat =False
207             self.options.stretch=False
208         elif self.options.copymode=="Repeated":
209             self.options.repeat =True
210             self.options.stretch=False
211         elif self.options.copymode=="Single, stretched":
212             self.options.repeat =False
213             self.options.stretch=True
214         elif self.options.copymode=="Repeated, stretched":
215             self.options.repeat =True
216             self.options.stretch=True
218         bbox=simpletransform.computeBBox(self.patterns.values())
219                     
220         if self.options.vertical:
221             #flipxy(bbox)...
222             bbox=(-bbox[3],-bbox[2],-bbox[1],-bbox[0])
224         width=bbox[1]-bbox[0]
225         dx=width+self.options.space
226         if dx < 0.01:
227             exit(_("The total length of the pattern is too small :\nPlease choose a larger object or set 'Space between copies' > 0"))
229         for id, node in self.patterns.iteritems():
230             if node.tag == inkex.addNS('path','svg') or node.tag=='path':
231                 d = node.get('d')
232                 p0 = cubicsuperpath.parsePath(d)
233                 if self.options.vertical:
234                     flipxy(p0)
236                 newp=[]
237                 for skelnode in self.skeletons.itervalues(): 
238                     self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
239                     if self.options.vertical:
240                         flipxy(self.curSekeleton)
241                     for comp in self.curSekeleton:
242                         p=copy.deepcopy(p0)
243                         self.skelcomp,self.lengths=linearize(comp)
244                         #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
245                         self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
247                         length=sum(self.lengths)
248                         xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
249                         yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
252                         if self.options.repeat:
253                             NbCopies=max(1,int(round((length+self.options.space)/dx)))
254                             width=dx*NbCopies
255                             if not self.skelcompIsClosed:
256                                 width-=self.options.space
257                             bbox=bbox[0],bbox[0]+width, bbox[2],bbox[3]
258                             new=[]
259                             for sub in p:
260                                 for i in range(0,NbCopies,1):
261                                     new.append(copy.deepcopy(sub))
262                                     offset(sub,dx,0)
263                             p=new
265                         for sub in p:
266                             offset(sub,xoffset,yoffset)
268                         if self.options.stretch:
269                             for sub in p:
270                                 stretch(sub,length/width,1,self.skelcomp[0])
272                         for sub in p:
273                             for ctlpt in sub:
274                                 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))
276                         if self.options.vertical:
277                             flipxy(p)
278                         newp+=p
280                 node.set('d', cubicsuperpath.formatPath(newp))
282 if __name__ == '__main__':
283     e = PathAlongPath()
284     e.affect()
286                     
287 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99