Code

Extensions. XAML export improvements.
[inkscape.git] / share / extensions / pathscatter.py
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
51             
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.
77     
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)
154                 
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]
159                 
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()
223         
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)
232                 
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)
246                 
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)
275                         
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)
281                     
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