Code

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