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 from lxml import etree
37 import copy, math, re, random
38 import gettext
39 _ = gettext.gettext
41 def zSort(inNode,idList):
42 sortedList=[]
43 theid = inNode.get("id")
44 if theid in idList:
45 sortedList.append(theid)
46 for child in inNode:
47 if len(sortedList)==len(idList):
48 break
49 sortedList+=zSort(child,idList)
50 return sortedList
53 def flipxy(path):
54 for pathcomp in path:
55 for ctl in pathcomp:
56 for pt in ctl:
57 tmp=pt[0]
58 pt[0]=-pt[1]
59 pt[1]=-tmp
61 def offset(pathcomp,dx,dy):
62 for ctl in pathcomp:
63 for pt in ctl:
64 pt[0]+=dx
65 pt[1]+=dy
67 def stretch(pathcomp,xscale,yscale,org):
68 for ctl in pathcomp:
69 for pt in ctl:
70 pt[0]=org[0]+(pt[0]-org[0])*xscale
71 pt[1]=org[1]+(pt[1]-org[1])*yscale
73 def linearize(p,tolerance=0.001):
74 '''
75 This function recieves a component of a 'cubicsuperpath' and returns two things:
76 The path subdivided in many straight segments, and an array containing the length of each segment.
78 We could work with bezier path as well, but bezier arc lengths are (re)computed for each point
79 in the deformed object. For complex paths, this might take a while.
80 '''
81 zero=0.000001
82 i=0
83 d=0
84 lengths=[]
85 while i<len(p)-1:
86 box = bezmisc.pointdistance(p[i ][1],p[i ][2])
87 box += bezmisc.pointdistance(p[i ][2],p[i+1][0])
88 box += bezmisc.pointdistance(p[i+1][0],p[i+1][1])
89 chord = bezmisc.pointdistance(p[i][1], p[i+1][1])
90 if (box - chord) > tolerance:
91 b1, b2 = bezmisc.beziersplitatt([p[i][1],p[i][2],p[i+1][0],p[i+1][1]], 0.5)
92 p[i ][2][0],p[i ][2][1]=b1[1]
93 p[i+1][0][0],p[i+1][0][1]=b2[2]
94 p.insert(i+1,[[b1[2][0],b1[2][1]],[b1[3][0],b1[3][1]],[b2[1][0],b2[1][1]]])
95 else:
96 d=(box+chord)/2
97 lengths.append(d)
98 i+=1
99 new=[p[i][1] for i in range(0,len(p)-1) if lengths[i]>zero]
100 new.append(p[-1][1])
101 lengths=[l for l in lengths if l>zero]
102 return(new,lengths)
104 class PathScatter(pathmodifier.Diffeo):
105 def __init__(self):
106 pathmodifier.Diffeo.__init__(self)
107 self.OptionParser.add_option("--title")
108 self.OptionParser.add_option("-n", "--noffset",
109 action="store", type="float",
110 dest="noffset", default=0.0, help="normal offset")
111 self.OptionParser.add_option("-t", "--toffset",
112 action="store", type="float",
113 dest="toffset", default=0.0, help="tangential offset")
114 self.OptionParser.add_option("-g", "--grouppick",
115 action="store", type="inkbool",
116 dest="grouppick", default=False,
117 help="if pattern is a group then randomly pick group members")
118 self.OptionParser.add_option("-m", "--pickmode",
119 action="store", type="string",
120 dest="pickmode", default="rand",
121 help="group pick mode (rand=random seq=sequentially)")
122 self.OptionParser.add_option("-f", "--follow",
123 action="store", type="inkbool",
124 dest="follow", default=True,
125 help="choose between wave or snake effect")
126 self.OptionParser.add_option("-s", "--stretch",
127 action="store", type="inkbool",
128 dest="stretch", default=True,
129 help="repeat the path to fit deformer's length")
130 self.OptionParser.add_option("-p", "--space",
131 action="store", type="float",
132 dest="space", default=0.0)
133 self.OptionParser.add_option("-v", "--vertical",
134 action="store", type="inkbool",
135 dest="vertical", default=False,
136 help="reference path is vertical")
137 self.OptionParser.add_option("-d", "--duplicate",
138 action="store", type="inkbool",
139 dest="duplicate", default=False,
140 help="duplicate pattern before deformation")
141 self.OptionParser.add_option("-c", "--copymode",
142 action="store", type="string",
143 dest="copymode", default="clone",
144 help="duplicate pattern before deformation")
145 self.OptionParser.add_option("--tab",
146 action="store", type="string",
147 dest="tab",
148 help="The selected UI-tab when OK was pressed")
150 def prepareSelectionList(self):
152 idList=self.options.ids
153 idList=zSort(self.document.getroot(),idList)
155 ##first selected->pattern, all but first selected-> skeletons
156 #id = self.options.ids[-1]
157 id = idList[-1]
158 self.patternNode=self.selected[id]
160 self.gNode = etree.Element('{http://www.w3.org/2000/svg}g')
161 self.patternNode.getparent().append(self.gNode)
163 if self.options.copymode=="copy":
164 duplist=self.duplicateNodes({id:self.patternNode})
165 self.patternNode = duplist.values()[0]
167 #TODO: allow 4th option: duplicate the first copy and clone the next ones.
168 if "%s"%self.options.copymode=="clone":
169 self.patternNode = etree.Element('{http://www.w3.org/2000/svg}use')
170 self.patternNode.set('{http://www.w3.org/1999/xlink}href',"#%s"%id)
171 self.gNode.append(self.patternNode)
173 self.skeletons=self.selected
174 del self.skeletons[id]
175 self.expandGroupsUnlinkClones(self.skeletons, True, False)
176 self.objectsToPaths(self.skeletons,False)
178 def lengthtotime(self,l):
179 '''
180 Recieves an arc length l, and returns the index of the segment in self.skelcomp
181 containing the coresponding point, to gether with the position of the point on this segment.
183 If the deformer is closed, do computations modulo the toal length.
184 '''
185 if self.skelcompIsClosed:
186 l=l % sum(self.lengths)
187 if l<=0:
188 return 0,l/self.lengths[0]
189 i=0
190 while (i<len(self.lengths)) and (self.lengths[i]<=l):
191 l-=self.lengths[i]
192 i+=1
193 t=l/self.lengths[min(i,len(self.lengths)-1)]
194 return i, t
196 def localTransformAt(self,s,follow=True):
197 '''
198 recieves a length, and returns the coresponding point and tangent of self.skelcomp
199 if follow is set to false, returns only the translation
200 '''
201 i,t=self.lengthtotime(s)
202 if i==len(self.skelcomp)-1:
203 x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
204 dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
205 dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
206 else:
207 x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
208 dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
209 dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
210 if follow:
211 mat=[[dx,-dy,x],[dy,dx,y]]
212 else:
213 mat=[[1,0,x],[0,1,y]]
214 return mat
217 def effect(self):
219 if len(self.options.ids)<2:
220 inkex.errormsg(_("This extension requires two selected paths."))
221 return
222 self.prepareSelectionList()
224 #center at (0,0)
225 bbox=pathmodifier.computeBBox([self.patternNode])
226 mat=[[1,0,-(bbox[0]+bbox[1])/2],[0,1,-(bbox[2]+bbox[3])/2]]
227 if self.options.vertical:
228 bbox=[-bbox[3],-bbox[2],bbox[0],bbox[1]]
229 mat=simpletransform.composeTransform([[0,-1,0],[1,0,0]],mat)
230 mat[1][2] += self.options.noffset
231 simpletransform.applyTransformToNode(mat,self.patternNode)
233 width=bbox[1]-bbox[0]
234 dx=width+self.options.space
236 #check if group and expand it
237 patternList = []
238 if self.options.grouppick and (self.patternNode.tag == inkex.addNS('g','svg') or self.patternNode.tag=='g') :
239 mat=simpletransform.parseTransform(self.patternNode.get("transform"))
240 for child in self.patternNode:
241 simpletransform.applyTransformToNode(mat,child)
242 patternList.append(child)
243 else :
244 patternList.append(self.patternNode)
245 #inkex.debug(patternList)
247 counter=0
248 for skelnode in self.skeletons.itervalues():
249 self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
250 for comp in self.curSekeleton:
251 self.skelcomp,self.lengths=linearize(comp)
252 #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
253 self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
255 length=sum(self.lengths)
256 if self.options.stretch:
257 dx=width+self.options.space
258 n=int((length-self.options.toffset+self.options.space)/dx)
259 if n>0:
260 dx=(length-self.options.toffset)/n
263 xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
264 yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
266 s=self.options.toffset
267 while s<=length:
268 mat=self.localTransformAt(s,self.options.follow)
269 if self.options.pickmode=="rand":
270 clone=copy.deepcopy(patternList[random.randint(0, len(patternList)-1)])
272 if self.options.pickmode=="seq":
273 clone=copy.deepcopy(patternList[counter])
274 counter=(counter+1)%len(patternList)
276 #!!!--> should it be given an id?
277 #seems to work without this!?!
278 myid = patternList[random.randint(0, len(patternList)-1)].tag.split('}')[-1]
279 clone.set("id", self.uniqueId(myid))
280 self.gNode.append(clone)
282 simpletransform.applyTransformToNode(mat,clone)
284 s+=dx
285 self.patternNode.getparent().remove(self.patternNode)
287 if __name__ == '__main__':
288 e = PathScatter()
289 e.affect()
292 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99