Code

more pyxml to lxml conversion.
[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 \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         ##first selected->pattern, all but first selected-> skeletons\r
122         id = self.options.ids[-1]\r
123         self.patterns={id:self.selected[id]}\r
124         if self.options.duplicate:\r
125             self.patterns=self.duplicateNodes(self.patterns)\r
126         self.expandGroupsUnlinkClones(self.patterns, True, True)\r
127         self.objectsToPaths(self.patterns)\r
128         del self.selected[id]\r
129 \r
130         self.skeletons=self.selected\r
131         self.expandGroupsUnlinkClones(self.skeletons, True, False)\r
132         self.objectsToPaths(self.skeletons)\r
133 \r
134     def lengthtotime(self,l):\r
135         '''\r
136         Recieves an arc length l, and returns the index of the segment in self.skelcomp \r
137         containing the coresponding point, to gether with the position of the point on this segment.\r
138 \r
139         If the deformer is closed, do computations modulo the toal length.\r
140         '''\r
141         if self.skelcompIsClosed:\r
142             l=l % sum(self.lengths)\r
143         if l<=0:\r
144             return 0,l/self.lengths[0]\r
145         i=0\r
146         while (i<len(self.lengths)) and (self.lengths[i]<=l):\r
147             l-=self.lengths[i]\r
148             i+=1\r
149         t=l/self.lengths[min(i,len(self.lengths)-1)]\r
150         return i, t\r
151 \r
152     def applyDiffeo(self,bpt,vects=()):\r
153         '''\r
154         The kernel of this stuff:\r
155         bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.\r
156         '''\r
157         s=bpt[0]-self.skelcomp[0][0]\r
158         i,t=self.lengthtotime(s)\r
159         if i==len(self.skelcomp)-1:\r
160             x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)\r
161             dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]\r
162             dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]\r
163         else:\r
164             x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)\r
165             dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]\r
166             dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]\r
167 \r
168         vx=0\r
169         vy=bpt[1]-self.skelcomp[0][1]\r
170         if self.options.wave:\r
171             bpt[0]=x+vx*dx\r
172             bpt[1]=y+vy+vx*dy\r
173         else:\r
174             bpt[0]=x+vx*dx-vy*dy\r
175             bpt[1]=y+vx*dy+vy*dx\r
176 \r
177         for v in vects:\r
178             vx=v[0]-self.skelcomp[0][0]-s\r
179             vy=v[1]-self.skelcomp[0][1]\r
180             if self.options.wave:\r
181                 v[0]=x+vx*dx\r
182                 v[1]=y+vy+vx*dy\r
183             else:\r
184                 v[0]=x+vx*dx-vy*dy\r
185                 v[1]=y+vx*dy+vy*dx\r
186 \r
187     def effect(self):\r
188         if len(self.options.ids)<2:\r
189             inkex.debug("This extension requires that you select two paths.")\r
190             return\r
191         self.prepareSelectionList()\r
192         self.options.wave = (self.options.kind=="Ribbon")\r
193         if self.options.copymode=="Single":\r
194             self.options.repeat =False\r
195             self.options.stretch=False\r
196         elif self.options.copymode=="Repeated":\r
197             self.options.repeat =True\r
198             self.options.stretch=False\r
199         elif self.options.copymode=="Single, stretched":\r
200             self.options.repeat =False\r
201             self.options.stretch=True\r
202         elif self.options.copymode=="Repeated, stretched":\r
203             self.options.repeat =True\r
204             self.options.stretch=True\r
205 \r
206         bbox=self.computeBBox(self.patterns)\r
207         if self.options.vertical:\r
208             #flipxy(bbox)...\r
209             bbox=(-bbox[3],-bbox[2],-bbox[1],-bbox[0])\r
210             \r
211         width=bbox[1]-bbox[0]\r
212         dx=width+self.options.space\r
213 \r
214         for id, node in self.patterns.iteritems():\r
215             if node.tag == inkex.addNS('path','svg'):\r
216                 d = node.get('d')\r
217                 p0 = cubicsuperpath.parsePath(d)\r
218                 if self.options.vertical:\r
219                     flipxy(p0)\r
220 \r
221                 newp=[]\r
222                 for skelnode in self.skeletons.itervalues(): \r
223                     self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))\r
224                     if self.options.vertical:\r
225                         flipxy(self.curSekeleton)\r
226                     for comp in self.curSekeleton:\r
227                         p=copy.deepcopy(p0)\r
228                         self.skelcomp,self.lengths=linearize(comp)\r
229                         #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!\r
230                         self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])\r
231 \r
232                         length=sum(self.lengths)\r
233                         xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset\r
234                         yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset\r
235 \r
236                         if self.options.repeat:\r
237                             NbCopies=max(1,int(round((length+self.options.space)/dx)))\r
238                             width=dx*NbCopies\r
239                             if not self.skelcompIsClosed:\r
240                                 width-=self.options.space\r
241                             bbox=bbox[0],bbox[0]+width,bbox[2],bbox[3]\r
242                             new=[]\r
243                             for sub in p:\r
244                                 for i in range(0,NbCopies,1):\r
245                                     new.append(copy.deepcopy(sub))\r
246                                     offset(sub,dx,0)\r
247                             p=new\r
248 \r
249                         for sub in p:\r
250                             offset(sub,xoffset,yoffset)\r
251 \r
252                         if self.options.stretch:\r
253                             for sub in p:\r
254                                 stretch(sub,length/width,1,self.skelcomp[0])\r
255 \r
256                         for sub in p:\r
257                             for ctlpt in sub:\r
258                                 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))\r
259 \r
260                         if self.options.vertical:\r
261                             flipxy(p)\r
262                         newp+=p\r
263 \r
264                 node.set('d', cubicsuperpath.formatPath(newp))\r
265 \r
266 e = PathAlongPath()\r
267 e.affect()\r
268 \r
269                     \r