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
38 import copy, math, re, random
40 def zSort(inNode,idList):
41 sortedList=[]
42 theid = inNode.get("id")
43 if theid in idList:
44 sortedList.append(theid)
45 for child in inNode:
46 if len(sortedList)==len(idList):
47 break
48 sortedList+=zSort(child,idList)
49 return sortedList
52 def flipxy(path):
53 for pathcomp in path:
54 for ctl in pathcomp:
55 for pt in ctl:
56 tmp=pt[0]
57 pt[0]=-pt[1]
58 pt[1]=-tmp
60 def offset(pathcomp,dx,dy):
61 for ctl in pathcomp:
62 for pt in ctl:
63 pt[0]+=dx
64 pt[1]+=dy
66 def stretch(pathcomp,xscale,yscale,org):
67 for ctl in pathcomp:
68 for pt in ctl:
69 pt[0]=org[0]+(pt[0]-org[0])*xscale
70 pt[1]=org[1]+(pt[1]-org[1])*yscale
72 def linearize(p,tolerance=0.001):
73 '''
74 This function recieves a component of a 'cubicsuperpath' and returns two things:
75 The path subdivided in many straight segments, and an array containing the length of each segment.
77 We could work with bezier path as well, but bezier arc lengths are (re)computed for each point
78 in the deformed object. For complex paths, this might take a while.
79 '''
80 zero=0.000001
81 i=0
82 d=0
83 lengths=[]
84 while i<len(p)-1:
85 box = bezmisc.pointdistance(p[i ][1],p[i ][2])
86 box += bezmisc.pointdistance(p[i ][2],p[i+1][0])
87 box += bezmisc.pointdistance(p[i+1][0],p[i+1][1])
88 chord = bezmisc.pointdistance(p[i][1], p[i+1][1])
89 if (box - chord) > tolerance:
90 b1, b2 = bezmisc.beziersplitatt([p[i][1],p[i][2],p[i+1][0],p[i+1][1]], 0.5)
91 p[i ][2][0],p[i ][2][1]=b1[1]
92 p[i+1][0][0],p[i+1][0][1]=b2[2]
93 p.insert(i+1,[[b1[2][0],b1[2][1]],[b1[3][0],b1[3][1]],[b2[1][0],b2[1][1]]])
94 else:
95 d=(box+chord)/2
96 lengths.append(d)
97 i+=1
98 new=[p[i][1] for i in range(0,len(p)-1) if lengths[i]>zero]
99 new.append(p[-1][1])
100 lengths=[l for l in lengths if l>zero]
101 return(new,lengths)
103 class PathScatter(pathmodifier.Diffeo):
104 def __init__(self):
105 pathmodifier.Diffeo.__init__(self)
106 self.OptionParser.add_option("--title")
107 self.OptionParser.add_option("-n", "--noffset",
108 action="store", type="float",
109 dest="noffset", default=0.0, help="normal offset")
110 self.OptionParser.add_option("-t", "--toffset",
111 action="store", type="float",
112 dest="toffset", default=0.0, help="tangential offset")
113 self.OptionParser.add_option("-f", "--follow",
114 action="store", type="inkbool",
115 dest="follow", default=True,
116 help="choose between wave or snake effect")
117 self.OptionParser.add_option("-s", "--stretch",
118 action="store", type="inkbool",
119 dest="stretch", default=True,
120 help="repeat the path to fit deformer's length")
121 self.OptionParser.add_option("-p", "--space",
122 action="store", type="float",
123 dest="space", default=0.0)
124 self.OptionParser.add_option("-v", "--vertical",
125 action="store", type="inkbool",
126 dest="vertical", default=False,
127 help="reference path is vertical")
128 self.OptionParser.add_option("-d", "--duplicate",
129 action="store", type="inkbool",
130 dest="duplicate", default=False,
131 help="duplicate pattern before deformation")
132 self.OptionParser.add_option("-c", "--copymode",
133 action="store", type="string",
134 dest="copymode", default="clone",
135 help="duplicate pattern before deformation")
137 def prepareSelectionList(self):
139 idList=self.options.ids
140 idList=zSort(self.document.getroot(),idList)
142 ##first selected->pattern, all but first selected-> skeletons
143 #id = self.options.ids[-1]
144 id = idList[-1]
145 self.patternNode=self.selected[id]
147 self.gNode = etree.Element('{http://www.w3.org/2000/svg}g')
148 self.patternNode.getparent().append(self.gNode)
150 if self.options.copymode=="copy":
151 duplist=self.duplicateNodes({id:self.patternNode})
152 self.patternNode = duplist.values()[0]
154 #TODO: allow 4th option: duplicate the first copy and clone the next ones.
155 if "%s"%self.options.copymode=="clone":
156 self.patternNode = etree.Element('{http://www.w3.org/2000/svg}use')
157 self.patternNode.set('{http://www.w3.org/1999/xlink}href',"#%s"%id)
158 self.gNode.append(self.patternNode)
160 self.skeletons=self.selected
161 del self.skeletons[id]
162 self.expandGroupsUnlinkClones(self.skeletons, True, False)
163 self.objectsToPaths(self.skeletons,False)
165 def lengthtotime(self,l):
166 '''
167 Recieves an arc length l, and returns the index of the segment in self.skelcomp
168 containing the coresponding point, to gether with the position of the point on this segment.
170 If the deformer is closed, do computations modulo the toal length.
171 '''
172 if self.skelcompIsClosed:
173 l=l % sum(self.lengths)
174 if l<=0:
175 return 0,l/self.lengths[0]
176 i=0
177 while (i<len(self.lengths)) and (self.lengths[i]<=l):
178 l-=self.lengths[i]
179 i+=1
180 t=l/self.lengths[min(i,len(self.lengths)-1)]
181 return i, t
183 def localTransformAt(self,s,follow=True):
184 '''
185 recieves a length, and returns the coresponding point and tangent of self.skelcomp
186 if follow is set to false, returns only the translation
187 '''
188 i,t=self.lengthtotime(s)
189 if i==len(self.skelcomp)-1:
190 x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
191 dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
192 dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
193 else:
194 x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
195 dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
196 dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
197 if follow:
198 mat=[[dx,-dy,x],[dy,dx,y]]
199 else:
200 mat=[[1,0,x],[0,1,y]]
201 return mat
204 def effect(self):
206 if len(self.options.ids)<2:
207 inkex.debug("This extension requires that you select two paths.")
208 return
209 self.prepareSelectionList()
211 #center at (0,0)
212 bbox=pathmodifier.computeBBox([self.patternNode])
213 mat=[[1,0,-(bbox[0]+bbox[1])/2],[0,1,-(bbox[2]+bbox[3])/2]]
214 if self.options.vertical:
215 bbox=[-bbox[3],-bbox[2],bbox[0],bbox[1]]
216 mat=simpletransform.composeTransform([[0,-1,0],[1,0,0]],mat)
217 mat[1][2] += self.options.noffset
218 simpletransform.applyTransformToNode(mat,self.patternNode)
220 width=bbox[1]-bbox[0]
221 dx=width+self.options.space
223 for skelnode in self.skeletons.itervalues():
224 self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
225 for comp in self.curSekeleton:
226 self.skelcomp,self.lengths=linearize(comp)
227 #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
228 self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
230 length=sum(self.lengths)
231 if self.options.stretch:
232 dx=width+self.options.space
233 n=int((length-self.options.toffset+self.options.space)/dx)
234 if n>0:
235 dx=(length-self.options.toffset)/n
238 xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
239 yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
241 s=self.options.toffset
242 while s<=length:
243 mat=self.localTransformAt(s,self.options.follow)
245 clone=copy.deepcopy(self.patternNode)
246 #!!!--> should it be given an id?
247 #seems to work without this!?!
248 myid = self.patternNode.tag.split('}')[-1]
249 clone.set("id", self.uniqueId(myid))
250 self.gNode.append(clone)
252 simpletransform.applyTransformToNode(mat,clone)
254 s+=dx
255 self.patternNode.getparent().remove(self.patternNode)
258 e = PathScatter()
259 e.affect()