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
37 import copy, math, re, random
39 def flipxy(path):
40 for pathcomp in path:
41 for ctl in pathcomp:
42 for pt in ctl:
43 tmp=pt[0]
44 pt[0]=-pt[1]
45 pt[1]=-tmp
47 def offset(pathcomp,dx,dy):
48 for ctl in pathcomp:
49 for pt in ctl:
50 pt[0]+=dx
51 pt[1]+=dy
53 def stretch(pathcomp,xscale,yscale,org):
54 for ctl in pathcomp:
55 for pt in ctl:
56 pt[0]=org[0]+(pt[0]-org[0])*xscale
57 pt[1]=org[1]+(pt[1]-org[1])*yscale
59 def linearize(p,tolerance=0.001):
60 '''
61 This function recieves a component of a 'cubicsuperpath' and returns two things:
62 The path subdivided in many straight segments, and an array containing the length of each segment.
64 We could work with bezier path as well, but bezier arc lengths are (re)computed for each point
65 in the deformed object. For complex paths, this might take a while.
66 '''
67 zero=0.000001
68 i=0
69 d=0
70 lengths=[]
71 while i<len(p)-1:
72 box = bezmisc.pointdistance(p[i ][1],p[i ][2])
73 box += bezmisc.pointdistance(p[i ][2],p[i+1][0])
74 box += bezmisc.pointdistance(p[i+1][0],p[i+1][1])
75 chord = bezmisc.pointdistance(p[i][1], p[i+1][1])
76 if (box - chord) > tolerance:
77 b1, b2 = bezmisc.beziersplitatt([p[i][1],p[i][2],p[i+1][0],p[i+1][1]], 0.5)
78 p[i ][2][0],p[i ][2][1]=b1[1]
79 p[i+1][0][0],p[i+1][0][1]=b2[2]
80 p.insert(i+1,[[b1[2][0],b1[2][1]],[b1[3][0],b1[3][1]],[b2[1][0],b2[1][1]]])
81 else:
82 d=(box+chord)/2
83 lengths.append(d)
84 i+=1
85 new=[p[i][1] for i in range(0,len(p)-1) if lengths[i]>zero]
86 new.append(p[-1][1])
87 lengths=[l for l in lengths if l>zero]
88 return(new,lengths)
90 class PathAlongPath(pathmodifier.Diffeo):
91 def __init__(self):
92 pathmodifier.Diffeo.__init__(self)
93 self.OptionParser.add_option("--title")
94 self.OptionParser.add_option("-n", "--noffset",
95 action="store", type="float",
96 dest="noffset", default=0.0, help="normal offset")
97 self.OptionParser.add_option("-t", "--toffset",
98 action="store", type="float",
99 dest="toffset", default=0.0, help="tangential offset")
100 self.OptionParser.add_option("-k", "--kind",
101 action="store", type="string",
102 dest="kind", default=True,
103 help="choose between wave or snake effect")
104 self.OptionParser.add_option("-c", "--copymode",
105 action="store", type="string",
106 dest="copymode", default=True,
107 help="repeat the path to fit deformer's length")
108 self.OptionParser.add_option("-p", "--space",
109 action="store", type="float",
110 dest="space", default=0.0)
111 self.OptionParser.add_option("-v", "--vertical",
112 action="store", type="inkbool",
113 dest="vertical", default=False,
114 help="reference path is vertical")
115 self.OptionParser.add_option("-d", "--duplicate",
116 action="store", type="inkbool",
117 dest="duplicate", default=False,
118 help="duplicate pattern before deformation")
120 def prepareSelectionList(self):
122 idList=self.options.ids
123 idList=pathmodifier.zSort(self.document.getroot(),idList)
124 id = idList[-1]
125 self.patterns={id:self.selected[id]}
127 ## ##first selected->pattern, all but first selected-> skeletons
128 ## id = self.options.ids[-1]
129 ## self.patterns={id:self.selected[id]}
131 if self.options.duplicate:
132 self.patterns=self.duplicateNodes(self.patterns)
133 self.expandGroupsUnlinkClones(self.patterns, True, True)
134 self.objectsToPaths(self.patterns)
135 del self.selected[id]
137 self.skeletons=self.selected
138 self.expandGroupsUnlinkClones(self.skeletons, True, False)
139 self.objectsToPaths(self.skeletons)
141 def lengthtotime(self,l):
142 '''
143 Recieves an arc length l, and returns the index of the segment in self.skelcomp
144 containing the coresponding point, to gether with the position of the point on this segment.
146 If the deformer is closed, do computations modulo the toal length.
147 '''
148 if self.skelcompIsClosed:
149 l=l % sum(self.lengths)
150 if l<=0:
151 return 0,l/self.lengths[0]
152 i=0
153 while (i<len(self.lengths)) and (self.lengths[i]<=l):
154 l-=self.lengths[i]
155 i+=1
156 t=l/self.lengths[min(i,len(self.lengths)-1)]
157 return i, t
159 def applyDiffeo(self,bpt,vects=()):
160 '''
161 The kernel of this stuff:
162 bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
163 '''
164 s=bpt[0]-self.skelcomp[0][0]
165 i,t=self.lengthtotime(s)
166 if i==len(self.skelcomp)-1:
167 x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
168 dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
169 dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
170 else:
171 x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
172 dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
173 dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
175 vx=0
176 vy=bpt[1]-self.skelcomp[0][1]
177 if self.options.wave:
178 bpt[0]=x+vx*dx
179 bpt[1]=y+vy+vx*dy
180 else:
181 bpt[0]=x+vx*dx-vy*dy
182 bpt[1]=y+vx*dy+vy*dx
184 for v in vects:
185 vx=v[0]-self.skelcomp[0][0]-s
186 vy=v[1]-self.skelcomp[0][1]
187 if self.options.wave:
188 v[0]=x+vx*dx
189 v[1]=y+vy+vx*dy
190 else:
191 v[0]=x+vx*dx-vy*dy
192 v[1]=y+vx*dy+vy*dx
194 def effect(self):
195 if len(self.options.ids)<2:
196 inkex.debug("This extension requires that you select two paths.")
197 return
198 self.prepareSelectionList()
199 self.options.wave = (self.options.kind=="Ribbon")
200 if self.options.copymode=="Single":
201 self.options.repeat =False
202 self.options.stretch=False
203 elif self.options.copymode=="Repeated":
204 self.options.repeat =True
205 self.options.stretch=False
206 elif self.options.copymode=="Single, stretched":
207 self.options.repeat =False
208 self.options.stretch=True
209 elif self.options.copymode=="Repeated, stretched":
210 self.options.repeat =True
211 self.options.stretch=True
213 bbox=simpletransform.computeBBox(self.patterns.values())
215 if self.options.vertical:
216 #flipxy(bbox)...
217 bbox=(-bbox[3],-bbox[2],-bbox[1],-bbox[0])
219 width=bbox[1]-bbox[0]
220 dx=width+self.options.space
222 for id, node in self.patterns.iteritems():
223 if node.tag == inkex.addNS('path','svg') or node.tag=='path':
224 d = node.get('d')
225 p0 = cubicsuperpath.parsePath(d)
226 if self.options.vertical:
227 flipxy(p0)
229 newp=[]
230 for skelnode in self.skeletons.itervalues():
231 self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
232 if self.options.vertical:
233 flipxy(self.curSekeleton)
234 for comp in self.curSekeleton:
235 p=copy.deepcopy(p0)
236 self.skelcomp,self.lengths=linearize(comp)
237 #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
238 self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
240 length=sum(self.lengths)
241 xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
242 yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
245 if self.options.repeat:
246 NbCopies=max(1,int(round((length+self.options.space)/dx)))
247 width=dx*NbCopies
248 if not self.skelcompIsClosed:
249 width-=self.options.space
250 bbox=bbox[0],bbox[0]+width, bbox[2],bbox[3]
251 new=[]
252 for sub in p:
253 for i in range(0,NbCopies,1):
254 new.append(copy.deepcopy(sub))
255 offset(sub,dx,0)
256 p=new
258 for sub in p:
259 offset(sub,xoffset,yoffset)
261 if self.options.stretch:
262 for sub in p:
263 stretch(sub,length/width,1,self.skelcomp[0])
265 for sub in p:
266 for ctlpt in sub:
267 self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2]))
269 if self.options.vertical:
270 flipxy(p)
271 newp+=p
273 node.set('d', cubicsuperpath.formatPath(newp))
275 e = PathAlongPath()
276 e.affect()