1 #!/usr/bin/env python
2 '''
3 Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 barraud@math.univ-lille1.fr
20 Quick description:
21 This script deforms an object (the pattern) along other paths (skeletons)...
22 The first selected object is the pattern
23 the last selected ones are the skeletons.
25 Imagine a straight horizontal line L in the middle of the bounding box of the pattern.
26 Consider the normal bundle of L: the collection of all the vertical lines meeting L.
27 Consider this as the initial state of the plane; in particular, think of the pattern
28 as painted on these lines.
30 Now move and bend L to make it fit a skeleton, and see what happens to the normals:
31 they move and rotate, deforming the pattern.
32 '''
34 import inkex, cubicsuperpath, bezmisc
35 import pathmodifier,simpletransform
36 import copy, math, re, random
37 import gettext
38 _ = gettext.gettext
40 def flipxy(path):
41 for pathcomp in path:
42 for ctl in pathcomp:
43 for pt in ctl:
44 tmp=pt[0]
45 pt[0]=-pt[1]
46 pt[1]=-tmp
48 def offset(pathcomp,dx,dy):
49 for ctl in pathcomp:
50 for pt in ctl:
51 pt[0]+=dx
52 pt[1]+=dy
54 def stretch(pathcomp,xscale,yscale,org):
55 for ctl in pathcomp:
56 for pt in ctl:
57 pt[0]=org[0]+(pt[0]-org[0])*xscale
58 pt[1]=org[1]+(pt[1]-org[1])*yscale
60 def linearize(p,tolerance=0.001):
61 '''
62 This function recieves a component of a 'cubicsuperpath' and returns two things:
63 The path subdivided in many straight segments, and an array containing the length of each segment.
65 We could work with bezier path as well, but bezier arc lengths are (re)computed for each point
66 in the deformed object. For complex paths, this might take a while.
67 '''
68 zero=0.000001
69 i=0
70 d=0
71 lengths=[]
72 while i<len(p)-1:
73 box = bezmisc.pointdistance(p[i ][1],p[i ][2])
74 box += bezmisc.pointdistance(p[i ][2],p[i+1][0])
75 box += bezmisc.pointdistance(p[i+1][0],p[i+1][1])
76 chord = bezmisc.pointdistance(p[i][1], p[i+1][1])
77 if (box - chord) > tolerance:
78 b1, b2 = bezmisc.beziersplitatt([p[i][1],p[i][2],p[i+1][0],p[i+1][1]], 0.5)
79 p[i ][2][0],p[i ][2][1]=b1[1]
80 p[i+1][0][0],p[i+1][0][1]=b2[2]
81 p.insert(i+1,[[b1[2][0],b1[2][1]],[b1[3][0],b1[3][1]],[b2[1][0],b2[1][1]]])
82 else:
83 d=(box+chord)/2
84 lengths.append(d)
85 i+=1
86 new=[p[i][1] for i in range(0,len(p)-1) if lengths[i]>zero]
87 new.append(p[-1][1])
88 lengths=[l for l in lengths if l>zero]
89 return(new,lengths)
91 class PathAlongPath(pathmodifier.Diffeo):
92 def __init__(self):
93 pathmodifier.Diffeo.__init__(self)
94 self.OptionParser.add_option("--title")
95 self.OptionParser.add_option("-n", "--noffset",
96 action="store", type="float",
97 dest="noffset", default=0.0, help="normal offset")
98 self.OptionParser.add_option("-t", "--toffset",
99 action="store", type="float",
100 dest="toffset", default=0.0, help="tangential offset")
101 self.OptionParser.add_option("-k", "--kind",
102 action="store", type="string",
103 dest="kind", default=True,
104 help="choose between wave or snake effect")
105 self.OptionParser.add_option("-c", "--copymode",
106 action="store", type="string",
107 dest="copymode", default=True,
108 help="repeat the path to fit deformer's length")
109 self.OptionParser.add_option("-p", "--space",
110 action="store", type="float",
111 dest="space", default=0.0)
112 self.OptionParser.add_option("-v", "--vertical",
113 action="store", type="inkbool",
114 dest="vertical", default=False,
115 help="reference path is vertical")
116 self.OptionParser.add_option("-d", "--duplicate",
117 action="store", type="inkbool",
118 dest="duplicate", default=False,
119 help="duplicate pattern before deformation")
120 self.OptionParser.add_option("--tab",
121 action="store", type="string",
122 dest="tab",
123 help="The selected UI-tab when OK was pressed")
125 def prepareSelectionList(self):
127 idList=self.options.ids
128 idList=pathmodifier.zSort(self.document.getroot(),idList)
129 id = idList[-1]
130 self.patterns={id:self.selected[id]}
132 ## ##first selected->pattern, all but first selected-> skeletons
133 ## id = self.options.ids[-1]
134 ## self.patterns={id:self.selected[id]}
136 if self.options.duplicate:
137 self.patterns=self.duplicateNodes(self.patterns)
138 self.expandGroupsUnlinkClones(self.patterns, True, True)
139 self.objectsToPaths(self.patterns)
140 del self.selected[id]
142 self.skeletons=self.selected
143 self.expandGroupsUnlinkClones(self.skeletons, True, False)
144 self.objectsToPaths(self.skeletons)
146 def lengthtotime(self,l):
147 '''
148 Recieves an arc length l, and returns the index of the segment in self.skelcomp
149 containing the coresponding point, to gether with the position of the point on this segment.
151 If the deformer is closed, do computations modulo the toal length.
152 '''
153 if self.skelcompIsClosed:
154 l=l % sum(self.lengths)
155 if l<=0:
156 return 0,l/self.lengths[0]
157 i=0
158 while (i<len(self.lengths)) and (self.lengths[i]<=l):
159 l-=self.lengths[i]
160 i+=1
161 t=l/self.lengths[min(i,len(self.lengths)-1)]
162 return i, t
164 def applyDiffeo(self,bpt,vects=()):
165 '''
166 The kernel of this stuff:
167 bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
168 '''
169 s=bpt[0]-self.skelcomp[0][0]
170 i,t=self.lengthtotime(s)
171 if i==len(self.skelcomp)-1:
172 x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
173 dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
174 dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
175 else:
176 x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
177 dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
178 dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
180 vx=0
181 vy=bpt[1]-self.skelcomp[0][1]
182 if self.options.wave:
183 bpt[0]=x+vx*dx
184 bpt[1]=y+vy+vx*dy
185 else:
186 bpt[0]=x+vx*dx-vy*dy
187 bpt[1]=y+vx*dy+vy*dx
189 for v in vects:
190 vx=v[0]-self.skelcomp[0][0]-s
191 vy=v[1]-self.skelcomp[0][1]
192 if self.options.wave:
193 v[0]=x+vx*dx
194 v[1]=y+vy+vx*dy
195 else:
196 v[0]=x+vx*dx-vy*dy
197 v[1]=y+vx*dy+vy*dx
199 def effect(self):
200 if len(self.options.ids)<2:
201 inkex.errormsg(_("This extension requires two selected paths."))
202 return
203 self.prepareSelectionList()
204 self.options.wave = (self.options.kind=="Ribbon")
205 if self.options.copymode=="Single":
206 self.options.repeat =False
207 self.options.stretch=False
208 elif self.options.copymode=="Repeated":
209 self.options.repeat =True
210 self.options.stretch=False
211 elif self.options.copymode=="Single, stretched":
212 self.options.repeat =False
213 self.options.stretch=True
214 elif self.options.copymode=="Repeated, stretched":
215 self.options.repeat =True
216 self.options.stretch=True
218 bbox=simpletransform.computeBBox(self.patterns.values())
220 if self.options.vertical:
221 #flipxy(bbox)...
222 bbox=(-bbox[3],-bbox[2],-bbox[1],-bbox[0])
224 width=bbox[1]-bbox[0]
225 dx=width+self.options.space
226 if dx < 0.01:
227 exit(_("The total length of the pattern is too small :\nPlease choose a larger object or set 'Space between copies' > 0"))
229 for id, node in self.patterns.iteritems():
230 if node.tag == inkex.addNS('path','svg') or node.tag=='path':
231 d = node.get('d')
232 p0 = cubicsuperpath.parsePath(d)
233 if self.options.vertical:
234 flipxy(p0)
236 newp=[]
237 for skelnode in self.skeletons.itervalues():
238 self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
239 if self.options.vertical:
240 flipxy(self.curSekeleton)
241 for comp in self.curSekeleton:
242 p=copy.deepcopy(p0)
243 self.skelcomp,self.lengths=linearize(comp)
244 #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
245 self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
247 length=sum(self.lengths)
248 xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
249 yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
252 if self.options.repeat:
253 NbCopies=max(1,int(round((length+self.options.space)/dx)))
254 width=dx*NbCopies
255 if not self.skelcompIsClosed:
256 width-=self.options.space
257 bbox=bbox[0],bbox[0]+width, bbox[2],bbox[3]
258 new=[]
259 for sub in p:
260 for i in range(0,NbCopies,1):
261 new.append(copy.deepcopy(sub))
262 offset(sub,dx,0)
263 p=new
265 for sub in p:
266 offset(sub,xoffset,yoffset)
268 if self.options.stretch:
269 for sub in p:
270 stretch(sub,length/width,1,self.skelcomp[0])
272 for sub in p:
273 for ctlpt in sub:
274 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))
276 if self.options.vertical:
277 flipxy(p)
278 newp+=p
280 node.set('d', cubicsuperpath.formatPath(newp))
282 if __name__ == '__main__':
283 e = PathAlongPath()
284 e.affect()
287 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99