Code

Partial fix for "make check" compilation failure.
[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("-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)
142                 
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)
220                 
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)
252                     
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