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