Code

share/extensions/*.py: Use gettext for (many) error messages.
[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])
219             
220         width=bbox[1]-bbox[0]
221         dx=width+self.options.space
223         for id, node in self.patterns.iteritems():
224             if node.tag == inkex.addNS('path','svg') or node.tag=='path':
225                 d = node.get('d')
226                 p0 = cubicsuperpath.parsePath(d)
227                 if self.options.vertical:
228                     flipxy(p0)
230                 newp=[]
231                 for skelnode in self.skeletons.itervalues(): 
232                     self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
233                     if self.options.vertical:
234                         flipxy(self.curSekeleton)
235                     for comp in self.curSekeleton:
236                         p=copy.deepcopy(p0)
237                         self.skelcomp,self.lengths=linearize(comp)
238                         #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
239                         self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
241                         length=sum(self.lengths)
242                         xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
243                         yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
246                         if self.options.repeat:
247                             NbCopies=max(1,int(round((length+self.options.space)/dx)))
248                             width=dx*NbCopies
249                             if not self.skelcompIsClosed:
250                                 width-=self.options.space
251                             bbox=bbox[0],bbox[0]+width, bbox[2],bbox[3]
252                             new=[]
253                             for sub in p:
254                                 for i in range(0,NbCopies,1):
255                                     new.append(copy.deepcopy(sub))
256                                     offset(sub,dx,0)
257                             p=new
259                         for sub in p:
260                             offset(sub,xoffset,yoffset)
262                         if self.options.stretch:
263                             for sub in p:
264                                 stretch(sub,length/width,1,self.skelcomp[0])
266                         for sub in p:
267                             for ctlpt in sub:
268                                 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))
270                         if self.options.vertical:
271                             flipxy(p)
272                         newp+=p
274                 node.set('d', cubicsuperpath.formatPath(newp))
276 if __name__ == '__main__':
277     e = PathAlongPath()
278     e.affect()
280                     
281 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99