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