Code

rename
[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, xml.xpath\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         self.prepareSelectionList()\r
189         self.options.wave = (self.options.kind=="Wave")\r
190         if self.options.copymode=="Single":\r
191             self.options.repeat =False\r
192             self.options.stretch=False\r
193         elif self.options.copymode=="Repeated":\r
194             self.options.repeat =True\r
195             self.options.stretch=False\r
196         elif self.options.copymode=="Single, stretched":\r
197             self.options.repeat =False\r
198             self.options.stretch=True\r
199         elif self.options.copymode=="Repeated, stretched":\r
200             self.options.repeat =True\r
201             self.options.stretch=True\r
202 \r
203         bbox=self.computeBBox(self.patterns)\r
204         if self.options.vertical:\r
205             #flipxy(bbox)...\r
206             bbox=(-bbox[3],-bbox[2],-bbox[1],-bbox[0])\r
207             \r
208         width=bbox[1]-bbox[0]\r
209         dx=width+self.options.space\r
210 \r
211         for id, node in self.patterns.iteritems():\r
212             if node.tagName == 'path':\r
213                 d = node.attributes.getNamedItem('d')\r
214                 p0 = cubicsuperpath.parsePath(d.value)\r
215                 if self.options.vertical:\r
216                     flipxy(p0)\r
217 \r
218                 newp=[]\r
219                 for skelnode in self.skeletons.itervalues(): \r
220                     self.curSekeleton=cubicsuperpath.parsePath(skelnode.getAttribute('d'))\r
221                     if self.options.vertical:\r
222                         flipxy(self.curSekeleton)\r
223                     for comp in self.curSekeleton:\r
224                         p=copy.deepcopy(p0)\r
225                         self.skelcomp,self.lengths=linearize(comp)\r
226         #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!\r
227                         self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])\r
228 \r
229                         length=sum(self.lengths)\r
230                         xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset\r
231                         yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset\r
232 \r
233                         if self.options.repeat:\r
234                             NbCopies=max(1,int(round((length+self.options.space)/dx)))\r
235                             width=dx*NbCopies\r
236                             if not self.skelcompIsClosed:\r
237                                 width-=self.options.space\r
238                             bbox=bbox[0],bbox[0]+width,bbox[2],bbox[3]\r
239                             new=[]\r
240                             for sub in p:\r
241                                 for i in range(0,NbCopies,1):\r
242                                     new.append(copy.deepcopy(sub))\r
243                                     offset(sub,dx,0)\r
244                             p=new\r
245 \r
246                         for sub in p:\r
247                             offset(sub,xoffset,yoffset)\r
248 \r
249                         if self.options.stretch:\r
250                             for sub in p:\r
251                                 stretch(sub,length/width,1,self.skelcomp[0])\r
252 \r
253                         for sub in p:\r
254                             for ctlpt in sub:\r
255                                 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))\r
256 \r
257                         if self.options.vertical:\r
258                             flipxy(p)\r
259                         newp+=p\r
260 \r
261                 d.value = cubicsuperpath.formatPath(newp)\r
262 \r
263 e = PathAlongPath()\r
264 e.affect()\r
265 \r
266                  \r