Code

enhancement by Rob Antonishen for Scatter Extension (Bug 617045)
[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")
146     def prepareSelectionList(self):
148         idList=self.options.ids
149         idList=zSort(self.document.getroot(),idList)
150                 
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]
155                 
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()
219         
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)
228                 
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)
242                 
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)
271                         
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)
277                     
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