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 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.tagName == 'path':\r
216 d = node.attributes.getNamedItem('d')\r
217 p0 = cubicsuperpath.parsePath(d.value)\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.getAttribute('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 d.value = cubicsuperpath.formatPath(newp)\r
265 \r
266 e = PathAlongPath()\r
267 e.affect()\r
268 \r
269 \r