Code

5bac2550b5bad2fdd34ab9cd23dda8f8d49e7185
[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
37 import copy, math, re, random
39 def flipxy(path):
40     for pathcomp in path:
41         for ctl in pathcomp:
42             for pt in ctl:
43                 tmp=pt[0]
44                 pt[0]=-pt[1]
45                 pt[1]=-tmp
47 def offset(pathcomp,dx,dy):
48     for ctl in pathcomp:
49         for pt in ctl:
50             pt[0]+=dx
51             pt[1]+=dy
53 def stretch(pathcomp,xscale,yscale,org):
54     for ctl in pathcomp:
55         for pt in ctl:
56             pt[0]=org[0]+(pt[0]-org[0])*xscale
57             pt[1]=org[1]+(pt[1]-org[1])*yscale
59 def linearize(p,tolerance=0.001):
60     '''
61     This function recieves a component of a 'cubicsuperpath' and returns two things:
62     The path subdivided in many straight segments, and an array containing the length of each segment.
63     
64     We could work with bezier path as well, but bezier arc lengths are (re)computed for each point 
65     in the deformed object. For complex paths, this might take a while.
66     '''
67     zero=0.000001
68     i=0
69     d=0
70     lengths=[]
71     while i<len(p)-1:
72         box  = bezmisc.pointdistance(p[i  ][1],p[i  ][2])
73         box += bezmisc.pointdistance(p[i  ][2],p[i+1][0])
74         box += bezmisc.pointdistance(p[i+1][0],p[i+1][1])
75         chord = bezmisc.pointdistance(p[i][1], p[i+1][1])
76         if (box - chord) > tolerance:
77             b1, b2 = bezmisc.beziersplitatt([p[i][1],p[i][2],p[i+1][0],p[i+1][1]], 0.5)
78             p[i  ][2][0],p[i  ][2][1]=b1[1]
79             p[i+1][0][0],p[i+1][0][1]=b2[2]
80             p.insert(i+1,[[b1[2][0],b1[2][1]],[b1[3][0],b1[3][1]],[b2[1][0],b2[1][1]]])
81         else:
82             d=(box+chord)/2
83             lengths.append(d)
84             i+=1
85     new=[p[i][1] for i in range(0,len(p)-1) if lengths[i]>zero]
86     new.append(p[-1][1])
87     lengths=[l for l in lengths if l>zero]
88     return(new,lengths)
90 class PathAlongPath(pathmodifier.Diffeo):
91     def __init__(self):
92         pathmodifier.Diffeo.__init__(self)
93         self.OptionParser.add_option("--title")
94         self.OptionParser.add_option("-n", "--noffset",
95                         action="store", type="float", 
96                         dest="noffset", default=0.0, help="normal offset")
97         self.OptionParser.add_option("-t", "--toffset",
98                         action="store", type="float", 
99                         dest="toffset", default=0.0, help="tangential offset")
100         self.OptionParser.add_option("-k", "--kind",
101                         action="store", type="string", 
102                         dest="kind", default=True,
103                         help="choose between wave or snake effect")
104         self.OptionParser.add_option("-c", "--copymode",
105                         action="store", type="string", 
106                         dest="copymode", default=True,
107                         help="repeat the path to fit deformer's length")
108         self.OptionParser.add_option("-p", "--space",
109                         action="store", type="float", 
110                         dest="space", default=0.0)
111         self.OptionParser.add_option("-v", "--vertical",
112                         action="store", type="inkbool", 
113                         dest="vertical", default=False,
114                         help="reference path is vertical")
115         self.OptionParser.add_option("-d", "--duplicate",
116                         action="store", type="inkbool", 
117                         dest="duplicate", default=False,
118                         help="duplicate pattern before deformation")
120     def prepareSelectionList(self):
122         idList=self.options.ids
123         idList=pathmodifier.zSort(self.document.getroot(),idList)
124         id = idList[-1]
125         self.patterns={id:self.selected[id]}
127 ##        ##first selected->pattern, all but first selected-> skeletons
128 ##        id = self.options.ids[-1]
129 ##        self.patterns={id:self.selected[id]}
131         if self.options.duplicate:
132             self.patterns=self.duplicateNodes(self.patterns)
133         self.expandGroupsUnlinkClones(self.patterns, True, True)
134         self.objectsToPaths(self.patterns)
135         del self.selected[id]
137         self.skeletons=self.selected
138         self.expandGroupsUnlinkClones(self.skeletons, True, False)
139         self.objectsToPaths(self.skeletons)
141     def lengthtotime(self,l):
142         '''
143         Recieves an arc length l, and returns the index of the segment in self.skelcomp 
144         containing the coresponding point, to gether with the position of the point on this segment.
146         If the deformer is closed, do computations modulo the toal length.
147         '''
148         if self.skelcompIsClosed:
149             l=l % sum(self.lengths)
150         if l<=0:
151             return 0,l/self.lengths[0]
152         i=0
153         while (i<len(self.lengths)) and (self.lengths[i]<=l):
154             l-=self.lengths[i]
155             i+=1
156         t=l/self.lengths[min(i,len(self.lengths)-1)]
157         return i, t
159     def applyDiffeo(self,bpt,vects=()):
160         '''
161         The kernel of this stuff:
162         bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
163         '''
164         s=bpt[0]-self.skelcomp[0][0]
165         i,t=self.lengthtotime(s)
166         if i==len(self.skelcomp)-1:
167             x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
168             dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
169             dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
170         else:
171             x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
172             dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
173             dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
175         vx=0
176         vy=bpt[1]-self.skelcomp[0][1]
177         if self.options.wave:
178             bpt[0]=x+vx*dx
179             bpt[1]=y+vy+vx*dy
180         else:
181             bpt[0]=x+vx*dx-vy*dy
182             bpt[1]=y+vx*dy+vy*dx
184         for v in vects:
185             vx=v[0]-self.skelcomp[0][0]-s
186             vy=v[1]-self.skelcomp[0][1]
187             if self.options.wave:
188                 v[0]=x+vx*dx
189                 v[1]=y+vy+vx*dy
190             else:
191                 v[0]=x+vx*dx-vy*dy
192                 v[1]=y+vx*dy+vy*dx
194     def effect(self):
195         if len(self.options.ids)<2:
196             inkex.debug("This extension requires that you select two paths.")
197             return
198         self.prepareSelectionList()
199         self.options.wave = (self.options.kind=="Ribbon")
200         if self.options.copymode=="Single":
201             self.options.repeat =False
202             self.options.stretch=False
203         elif self.options.copymode=="Repeated":
204             self.options.repeat =True
205             self.options.stretch=False
206         elif self.options.copymode=="Single, stretched":
207             self.options.repeat =False
208             self.options.stretch=True
209         elif self.options.copymode=="Repeated, stretched":
210             self.options.repeat =True
211             self.options.stretch=True
213         bbox=simpletransform.computeBBox(self.patterns.values())
214                     
215         if self.options.vertical:
216             #flipxy(bbox)...
217             bbox=(-bbox[3],-bbox[2],-bbox[1],-bbox[0])
218             
219         width=bbox[1]-bbox[0]
220         dx=width+self.options.space
222         for id, node in self.patterns.iteritems():
223             if node.tag == inkex.addNS('path','svg') or node.tag=='path':
224                 d = node.get('d')
225                 p0 = cubicsuperpath.parsePath(d)
226                 if self.options.vertical:
227                     flipxy(p0)
229                 newp=[]
230                 for skelnode in self.skeletons.itervalues(): 
231                     self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
232                     if self.options.vertical:
233                         flipxy(self.curSekeleton)
234                     for comp in self.curSekeleton:
235                         p=copy.deepcopy(p0)
236                         self.skelcomp,self.lengths=linearize(comp)
237                         #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
238                         self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
240                         length=sum(self.lengths)
241                         xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
242                         yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
245                         if self.options.repeat:
246                             NbCopies=max(1,int(round((length+self.options.space)/dx)))
247                             width=dx*NbCopies
248                             if not self.skelcompIsClosed:
249                                 width-=self.options.space
250                             bbox=bbox[0],bbox[0]+width, bbox[2],bbox[3]
251                             new=[]
252                             for sub in p:
253                                 for i in range(0,NbCopies,1):
254                                     new.append(copy.deepcopy(sub))
255                                     offset(sub,dx,0)
256                             p=new
258                         for sub in p:
259                             offset(sub,xoffset,yoffset)
261                         if self.options.stretch:
262                             for sub in p:
263                                 stretch(sub,length/width,1,self.skelcomp[0])
265                         for sub in p:
266                             for ctlpt in sub:
267                                 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))
269                         if self.options.vertical:
270                             flipxy(p)
271                         newp+=p
273                 node.set('d', cubicsuperpath.formatPath(newp))
275 e = PathAlongPath()
276 e.affect()
278