be2e5bc19e863a5cc132051c0f0349805c0f5446
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
222 if dx < 0.01:
223 exit(_("The total length of the pattern is too small :\nPlease choose a larger object or set 'Space between copies' > 0"))
225 for id, node in self.patterns.iteritems():
226 if node.tag == inkex.addNS('path','svg') or node.tag=='path':
227 d = node.get('d')
228 p0 = cubicsuperpath.parsePath(d)
229 if self.options.vertical:
230 flipxy(p0)
232 newp=[]
233 for skelnode in self.skeletons.itervalues():
234 self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
235 if self.options.vertical:
236 flipxy(self.curSekeleton)
237 for comp in self.curSekeleton:
238 p=copy.deepcopy(p0)
239 self.skelcomp,self.lengths=linearize(comp)
240 #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
241 self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
243 length=sum(self.lengths)
244 xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
245 yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
248 if self.options.repeat:
249 NbCopies=max(1,int(round((length+self.options.space)/dx)))
250 width=dx*NbCopies
251 if not self.skelcompIsClosed:
252 width-=self.options.space
253 bbox=bbox[0],bbox[0]+width, bbox[2],bbox[3]
254 new=[]
255 for sub in p:
256 for i in range(0,NbCopies,1):
257 new.append(copy.deepcopy(sub))
258 offset(sub,dx,0)
259 p=new
261 for sub in p:
262 offset(sub,xoffset,yoffset)
264 if self.options.stretch:
265 for sub in p:
266 stretch(sub,length/width,1,self.skelcomp[0])
268 for sub in p:
269 for ctlpt in sub:
270 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))
272 if self.options.vertical:
273 flipxy(p)
274 newp+=p
276 node.set('d', cubicsuperpath.formatPath(newp))
278 if __name__ == '__main__':
279 e = PathAlongPath()
280 e.affect()
283 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99