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("-f", "--follow",
115 action="store", type="inkbool",
116 dest="follow", default=True,
117 help="choose between wave or snake effect")
118 self.OptionParser.add_option("-s", "--stretch",
119 action="store", type="inkbool",
120 dest="stretch", default=True,
121 help="repeat the path to fit deformer's length")
122 self.OptionParser.add_option("-p", "--space",
123 action="store", type="float",
124 dest="space", default=0.0)
125 self.OptionParser.add_option("-v", "--vertical",
126 action="store", type="inkbool",
127 dest="vertical", default=False,
128 help="reference path is vertical")
129 self.OptionParser.add_option("-d", "--duplicate",
130 action="store", type="inkbool",
131 dest="duplicate", default=False,
132 help="duplicate pattern before deformation")
133 self.OptionParser.add_option("-c", "--copymode",
134 action="store", type="string",
135 dest="copymode", default="clone",
136 help="duplicate pattern before deformation")
138 def prepareSelectionList(self):
140 idList=self.options.ids
141 idList=zSort(self.document.getroot(),idList)
143 ##first selected->pattern, all but first selected-> skeletons
144 #id = self.options.ids[-1]
145 id = idList[-1]
146 self.patternNode=self.selected[id]
148 self.gNode = etree.Element('{http://www.w3.org/2000/svg}g')
149 self.patternNode.getparent().append(self.gNode)
151 if self.options.copymode=="copy":
152 duplist=self.duplicateNodes({id:self.patternNode})
153 self.patternNode = duplist.values()[0]
155 #TODO: allow 4th option: duplicate the first copy and clone the next ones.
156 if "%s"%self.options.copymode=="clone":
157 self.patternNode = etree.Element('{http://www.w3.org/2000/svg}use')
158 self.patternNode.set('{http://www.w3.org/1999/xlink}href',"#%s"%id)
159 self.gNode.append(self.patternNode)
161 self.skeletons=self.selected
162 del self.skeletons[id]
163 self.expandGroupsUnlinkClones(self.skeletons, True, False)
164 self.objectsToPaths(self.skeletons,False)
166 def lengthtotime(self,l):
167 '''
168 Recieves an arc length l, and returns the index of the segment in self.skelcomp
169 containing the coresponding point, to gether with the position of the point on this segment.
171 If the deformer is closed, do computations modulo the toal length.
172 '''
173 if self.skelcompIsClosed:
174 l=l % sum(self.lengths)
175 if l<=0:
176 return 0,l/self.lengths[0]
177 i=0
178 while (i<len(self.lengths)) and (self.lengths[i]<=l):
179 l-=self.lengths[i]
180 i+=1
181 t=l/self.lengths[min(i,len(self.lengths)-1)]
182 return i, t
184 def localTransformAt(self,s,follow=True):
185 '''
186 recieves a length, and returns the coresponding point and tangent of self.skelcomp
187 if follow is set to false, returns only the translation
188 '''
189 i,t=self.lengthtotime(s)
190 if i==len(self.skelcomp)-1:
191 x,y=bezmisc.tpoint(self.skelcomp[i-1],self.skelcomp[i],1+t)
192 dx=(self.skelcomp[i][0]-self.skelcomp[i-1][0])/self.lengths[-1]
193 dy=(self.skelcomp[i][1]-self.skelcomp[i-1][1])/self.lengths[-1]
194 else:
195 x,y=bezmisc.tpoint(self.skelcomp[i],self.skelcomp[i+1],t)
196 dx=(self.skelcomp[i+1][0]-self.skelcomp[i][0])/self.lengths[i]
197 dy=(self.skelcomp[i+1][1]-self.skelcomp[i][1])/self.lengths[i]
198 if follow:
199 mat=[[dx,-dy,x],[dy,dx,y]]
200 else:
201 mat=[[1,0,x],[0,1,y]]
202 return mat
205 def effect(self):
207 if len(self.options.ids)<2:
208 inkex.errormsg(_("This extension requires two selected paths."))
209 return
210 self.prepareSelectionList()
212 #center at (0,0)
213 bbox=pathmodifier.computeBBox([self.patternNode])
214 mat=[[1,0,-(bbox[0]+bbox[1])/2],[0,1,-(bbox[2]+bbox[3])/2]]
215 if self.options.vertical:
216 bbox=[-bbox[3],-bbox[2],bbox[0],bbox[1]]
217 mat=simpletransform.composeTransform([[0,-1,0],[1,0,0]],mat)
218 mat[1][2] += self.options.noffset
219 simpletransform.applyTransformToNode(mat,self.patternNode)
221 width=bbox[1]-bbox[0]
222 dx=width+self.options.space
224 for skelnode in self.skeletons.itervalues():
225 self.curSekeleton=cubicsuperpath.parsePath(skelnode.get('d'))
226 for comp in self.curSekeleton:
227 self.skelcomp,self.lengths=linearize(comp)
228 #!!!!>----> TODO: really test if path is closed! end point==start point is not enough!
229 self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1])
231 length=sum(self.lengths)
232 if self.options.stretch:
233 dx=width+self.options.space
234 n=int((length-self.options.toffset+self.options.space)/dx)
235 if n>0:
236 dx=(length-self.options.toffset)/n
239 xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset
240 yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset
242 s=self.options.toffset
243 while s<=length:
244 mat=self.localTransformAt(s,self.options.follow)
246 clone=copy.deepcopy(self.patternNode)
247 #!!!--> should it be given an id?
248 #seems to work without this!?!
249 myid = self.patternNode.tag.split('}')[-1]
250 clone.set("id", self.uniqueId(myid))
251 self.gNode.append(clone)
253 simpletransform.applyTransformToNode(mat,clone)
255 s+=dx
256 self.patternNode.getparent().remove(self.patternNode)
258 if __name__ == '__main__':
259 e = PathScatter()
260 e.affect()
263 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99