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")
121 def prepareSelectionList(self):
123 idList=self.options.ids
124 idList=pathmodifier.zSort(self.document.getroot(),idList)
125 id = idList[-1]
126 self.patterns={id:self.selected[id]}
128 ## ##first selected->pattern, all but first selected-> skeletons
129 ## id = self.options.ids[-1]
130 ## self.patterns={id:self.selected[id]}
132 if self.options.duplicate:
133 self.patterns=self.duplicateNodes(self.patterns)
134 self.expandGroupsUnlinkClones(self.patterns, True, True)
135 self.objectsToPaths(self.patterns)
136 del self.selected[id]
138 self.skeletons=self.selected
139 self.expandGroupsUnlinkClones(self.skeletons, True, False)
140 self.objectsToPaths(self.skeletons)
142 def lengthtotime(self,l):
143 '''
144 Recieves an arc length l, and returns the index of the segment in self.skelcomp
145 containing the coresponding point, to gether with the position of the point on this segment.
147 If the deformer is closed, do computations modulo the toal length.
148 '''
149 if self.skelcompIsClosed:
150 l=l % sum(self.lengths)
151 if l<=0:
152 return 0,l/self.lengths[0]
153 i=0
154 while (i<len(self.lengths)) and (self.lengths[i]<=l):
155 l-=self.lengths[i]
156 i+=1
157 t=l/self.lengths[min(i,len(self.lengths)-1)]
158 return i, t
160 def applyDiffeo(self,bpt,vects=()):
161 '''
162 The kernel of this stuff:
163 bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
164 '''
165 s=bpt[0]-self.skelcomp[0][0]
166 i,t=self.lengthtotime(s)
167 if i==len(self.skelcomp)-1:
168 x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
169 dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
170 dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
171 else:
172 x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
173 dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
174 dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
176 vx=0
177 vy=bpt[1]-self.skelcomp[0][1]
178 if self.options.wave:
179 bpt[0]=x+vx*dx
180 bpt[1]=y+vy+vx*dy
181 else:
182 bpt[0]=x+vx*dx-vy*dy
183 bpt[1]=y+vx*dy+vy*dx
185 for v in vects:
186 vx=v[0]-self.skelcomp[0][0]-s
187 vy=v[1]-self.skelcomp[0][1]
188 if self.options.wave:
189 v[0]=x+vx*dx
190 v[1]=y+vy+vx*dy
191 else:
192 v[0]=x+vx*dx-vy*dy
193 v[1]=y+vx*dy+vy*dx
195 def effect(self):
196 if len(self.options.ids)<2:
197 inkex.errormsg(_("This extension requires two selected paths."))
198 return
199 self.prepareSelectionList()
200 self.options.wave = (self.options.kind=="Ribbon")
201 if self.options.copymode=="Single":
202 self.options.repeat =False
203 self.options.stretch=False
204 elif self.options.copymode=="Repeated":
205 self.options.repeat =True
206 self.options.stretch=False
207 elif self.options.copymode=="Single, stretched":
208 self.options.repeat =False
209 self.options.stretch=True
210 elif self.options.copymode=="Repeated, stretched":
211 self.options.repeat =True
212 self.options.stretch=True
214 bbox=simpletransform.computeBBox(self.patterns.values())
216 if self.options.vertical:
217 #flipxy(bbox)...
218 bbox=(-bbox[3],-bbox[2],-bbox[1],-bbox[0])
220 width=bbox[1]-bbox[0]
221 dx=width+self.options.space
223 for id, node in self.patterns.iteritems():
224 if node.tag == inkex.addNS('path','svg') or node.tag=='path':
225 d = node.get('d')
226 p0 = cubicsuperpath.parsePath(d)
227 if self.options.vertical:
228 flipxy(p0)
230 newp=[]
231 for skelnode in self.skeletons.itervalues():
232 self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
233 if self.options.vertical:
234 flipxy(self.curSekeleton)
235 for comp in self.curSekeleton:
236 p=copy.deepcopy(p0)
237 self.skelcomp,self.lengths=linearize(comp)
238 #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
239 self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
241 length=sum(self.lengths)
242 xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
243 yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
246 if self.options.repeat:
247 NbCopies=max(1,int(round((length+self.options.space)/dx)))
248 width=dx*NbCopies
249 if not self.skelcompIsClosed:
250 width-=self.options.space
251 bbox=bbox[0],bbox[0]+width, bbox[2],bbox[3]
252 new=[]
253 for sub in p:
254 for i in range(0,NbCopies,1):
255 new.append(copy.deepcopy(sub))
256 offset(sub,dx,0)
257 p=new
259 for sub in p:
260 offset(sub,xoffset,yoffset)
262 if self.options.stretch:
263 for sub in p:
264 stretch(sub,length/width,1,self.skelcomp[0])
266 for sub in p:
267 for ctlpt in sub:
268 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))
270 if self.options.vertical:
271 flipxy(p)
272 newp+=p
274 node.set('d', cubicsuperpath.formatPath(newp))
276 if __name__ == '__main__':
277 e = PathAlongPath()
278 e.affect()
281 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99