Code

svn propset svn:eol-style native *.py
[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
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
50             
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.
76     
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)
141                 
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)
219                 
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)
251                     
252                     simpletransform.applyTransformToNode(mat,clone)
254                     s+=dx
255         self.patternNode.getparent().remove(self.patternNode)
258 e = PathScatter()
259 e.affect()
261