a7a77e1eca9fc1b1547e03a95dc6afec947c8d5a
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")
146 def prepareSelectionList(self):
148 idList=self.options.ids
149 idList=zSort(self.document.getroot(),idList)
151 ##first selected->pattern, all but first selected-> skeletons
152 #id = self.options.ids[-1]
153 id = idList[-1]
154 self.patternNode=self.selected[id]
156 self.gNode = etree.Element('{http://www.w3.org/2000/svg}g')
157 self.patternNode.getparent().append(self.gNode)
159 if self.options.copymode=="copy":
160 duplist=self.duplicateNodes({id:self.patternNode})
161 self.patternNode = duplist.values()[0]
163 #TODO: allow 4th option: duplicate the first copy and clone the next ones.
164 if "%s"%self.options.copymode=="clone":
165 self.patternNode = etree.Element('{http://www.w3.org/2000/svg}use')
166 self.patternNode.set('{http://www.w3.org/1999/xlink}href',"#%s"%id)
167 self.gNode.append(self.patternNode)
169 self.skeletons=self.selected
170 del self.skeletons[id]
171 self.expandGroupsUnlinkClones(self.skeletons, True, False)
172 self.objectsToPaths(self.skeletons,False)
174 def lengthtotime(self,l):
175 '''
176 Recieves an arc length l, and returns the index of the segment in self.skelcomp
177 containing the coresponding point, to gether with the position of the point on this segment.
179 If the deformer is closed, do computations modulo the toal length.
180 '''
181 if self.skelcompIsClosed:
182 l=l % sum(self.lengths)
183 if l<=0:
184 return 0,l/self.lengths[0]
185 i=0
186 while (i<len(self.lengths)) and (self.lengths[i]<=l):
187 l-=self.lengths[i]
188 i+=1
189 t=l/self.lengths[min(i,len(self.lengths)-1)]
190 return i, t
192 def localTransformAt(self,s,follow=True):
193 '''
194 recieves a length, and returns the coresponding point and tangent of self.skelcomp
195 if follow is set to false, returns only the translation
196 '''
197 i,t=self.lengthtotime(s)
198 if i==len(self.skelcomp)-1:
199 x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
200 dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
201 dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
202 else:
203 x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
204 dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
205 dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
206 if follow:
207 mat=[[dx,-dy,x],[dy,dx,y]]
208 else:
209 mat=[[1,0,x],[0,1,y]]
210 return mat
213 def effect(self):
215 if len(self.options.ids)<2:
216 inkex.errormsg(_("This extension requires two selected paths."))
217 return
218 self.prepareSelectionList()
220 #center at (0,0)
221 bbox=pathmodifier.computeBBox([self.patternNode])
222 mat=[[1,0,-(bbox[0]+bbox[1])/2],[0,1,-(bbox[2]+bbox[3])/2]]
223 if self.options.vertical:
224 bbox=[-bbox[3],-bbox[2],bbox[0],bbox[1]]
225 mat=simpletransform.composeTransform([[0,-1,0],[1,0,0]],mat)
226 mat[1][2] += self.options.noffset
227 simpletransform.applyTransformToNode(mat,self.patternNode)
229 width=bbox[1]-bbox[0]
230 dx=width+self.options.space
232 #check if group and expand it
233 patternList = []
234 if self.options.grouppick and (self.patternNode.tag == inkex.addNS('g','svg') or self.patternNode.tag=='g') :
235 mat=simpletransform.parseTransform(self.patternNode.get("transform"))
236 for child in self.patternNode:
237 simpletransform.applyTransformToNode(mat,child)
238 patternList.append(child)
239 else :
240 patternList.append(self.patternNode)
241 #inkex.debug(patternList)
243 counter=0
244 for skelnode in self.skeletons.itervalues():
245 self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
246 for comp in self.curSekeleton:
247 self.skelcomp,self.lengths=linearize(comp)
248 #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
249 self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
251 length=sum(self.lengths)
252 if self.options.stretch:
253 dx=width+self.options.space
254 n=int((length-self.options.toffset+self.options.space)/dx)
255 if n>0:
256 dx=(length-self.options.toffset)/n
259 xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
260 yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
262 s=self.options.toffset
263 while s<=length:
264 mat=self.localTransformAt(s,self.options.follow)
265 if self.options.pickmode=="rand":
266 clone=copy.deepcopy(patternList[random.randint(0, len(patternList)-1)])
268 if self.options.pickmode=="seq":
269 clone=copy.deepcopy(patternList[counter])
270 counter=(counter+1)%len(patternList)
272 #!!!--> should it be given an id?
273 #seems to work without this!?!
274 myid = patternList[random.randint(0, len(patternList)-1)].tag.split('}')[-1]
275 clone.set("id", self.uniqueId(myid))
276 self.gNode.append(clone)
278 simpletransform.applyTransformToNode(mat,clone)
280 s+=dx
281 self.patternNode.getparent().remove(self.patternNode)
283 if __name__ == '__main__':
284 e = PathScatter()
285 e.affect()
288 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99