From 9e9083d5b1ba585665bf2f97eb2fba6a6e4d11eb Mon Sep 17 00:00:00 2001 From: buliabyak Date: Thu, 23 Nov 2006 20:29:45 +0000 Subject: [PATCH] add Pattern on Path (patch 1594529) --- share/extensions/Makefile.am | 8 +- share/extensions/pathalongpath.inx | 38 +++ share/extensions/pathalongpath.py | 266 ++++++++++++++++++++ share/extensions/pathmodifier.py | 376 +++++++++++++++++++++++++++++ share/extensions/rubberstretch.py | 79 ++++++ 5 files changed, 765 insertions(+), 2 deletions(-) create mode 100644 share/extensions/pathalongpath.inx create mode 100644 share/extensions/pathalongpath.py create mode 100644 share/extensions/pathmodifier.py create mode 100644 share/extensions/rubberstretch.py diff --git a/share/extensions/Makefile.am b/share/extensions/Makefile.am index cbcba7f89..8a6ad980d 100644 --- a/share/extensions/Makefile.am +++ b/share/extensions/Makefile.am @@ -80,7 +80,10 @@ extensions = \ color_morelight.py\ color_lesslight.py\ color_morehue.py\ - color_lesshue.py + color_lesshue.py\ + pathalongpath.py\ + rubberstretch.py\ + pathmodifier.py otherstuff = \ @@ -158,7 +161,8 @@ modules = \ color_morelight.inx\ color_lesslight.inx\ color_morehue.inx\ - color_lesshue.inx + color_lesshue.inx\ + pathalongpath.inx extension_SCRIPTS = \ diff --git a/share/extensions/pathalongpath.inx b/share/extensions/pathalongpath.inx new file mode 100644 index 000000000..303427a82 --- /dev/null +++ b/share/extensions/pathalongpath.inx @@ -0,0 +1,38 @@ + + <_name>Pattern along Path + math.univ-lille1.barraud.pathdeform + pathmodifier.py + pathalongpath.py + inkex.py + This effect bends a pattern object along an arbitrary "skeleton" path. The pattern can be a path or a group of paths. First, select the pattern object; then add to selection the skeleton path; then call this effect. + + + Single + Single, stretched + Repeated + Repeated, stretched + + + + Snake + Wave + + + 0.0 + + 0.0 + 0.0 + + false + true + + + + + + + + + diff --git a/share/extensions/pathalongpath.py b/share/extensions/pathalongpath.py new file mode 100644 index 000000000..598f3c817 --- /dev/null +++ b/share/extensions/pathalongpath.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +''' +Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +barraud@math.univ-lille1.fr + +Quick description: +This script deforms an object (the pattern) along other paths (skeletons)... +The first selected object is the pattern +the last selected ones are the skeletons. + +Imagine a straight horizontal line L in the middle of the bounding box of the pattern. +Consider the normal bundle of L: the collection of all the vertical lines meeting L. +Consider this as the initial state of the plane; in particular, think of the pattern +as painted on these lines. + +Now move and bend L to make it fit a skeleton, and see what happens to the normals: +they move and rotate, deforming the pattern. +''' + +import inkex, cubicsuperpath, bezmisc +import pathmodifier + +import copy, math, re, random, xml.xpath + +def flipxy(path): + for pathcomp in path: + for ctl in pathcomp: + for pt in ctl: + tmp=pt[0] + pt[0]=-pt[1] + pt[1]=-tmp + +def offset(pathcomp,dx,dy): + for ctl in pathcomp: + for pt in ctl: + pt[0]+=dx + pt[1]+=dy + +def stretch(pathcomp,xscale,yscale,org): + for ctl in pathcomp: + for pt in ctl: + pt[0]=org[0]+(pt[0]-org[0])*xscale + pt[1]=org[1]+(pt[1]-org[1])*yscale + +def linearize(p,tolerance=0.001): + ''' + This function recieves a component of a 'cubicsuperpath' and returns two things: + The path subdivided in many straight segments, and an array containing the length of each segment. + + We could work with bezier path as well, but bezier arc lengths are (re)computed for each point + in the deformed object. For complex paths, this might take a while. + ''' + zero=0.000001 + i=0 + d=0 + lengths=[] + while i tolerance: + b1, b2 = bezmisc.beziersplitatt([p[i][1],p[i][2],p[i+1][0],p[i+1][1]], 0.5) + p[i ][2][0],p[i ][2][1]=b1[1] + p[i+1][0][0],p[i+1][0][1]=b2[2] + p.insert(i+1,[[b1[2][0],b1[2][1]],[b1[3][0],b1[3][1]],[b2[1][0],b2[1][1]]]) + else: + d=(box+chord)/2 + lengths.append(d) + i+=1 + new=[p[i][1] for i in range(0,len(p)-1) if lengths[i]>zero] + new.append(p[-1][1]) + lengths=[l for l in lengths if l>zero] + return(new,lengths) + +class PathAlongPath(pathmodifier.Diffeo): + def __init__(self): + pathmodifier.Diffeo.__init__(self) + self.OptionParser.add_option("--title") + self.OptionParser.add_option("-n", "--noffset", + action="store", type="float", + dest="noffset", default=0.0, help="normal offset") + self.OptionParser.add_option("-t", "--toffset", + action="store", type="float", + dest="toffset", default=0.0, help="tangential offset") + self.OptionParser.add_option("-k", "--kind", + action="store", type="string", + dest="kind", default=True, + help="choose between wave or snake effect") + self.OptionParser.add_option("-c", "--copymode", + action="store", type="string", + dest="copymode", default=True, + help="repeat the path to fit deformer's length") + self.OptionParser.add_option("-p", "--space", + action="store", type="float", + dest="space", default=0.0) + self.OptionParser.add_option("-v", "--vertical", + action="store", type="inkbool", + dest="vertical", default=False, + help="reference path is vertical") + self.OptionParser.add_option("-d", "--duplicate", + action="store", type="inkbool", + dest="duplicate", default=False, + help="duplicate pattern before deformation") + + def prepareSelectionList(self): +##first selected->pattern, all but first selected-> skeletons + id = self.options.ids[-1] + self.patterns={id:self.selected[id]} + if self.options.duplicate: + self.patterns=self.duplicateNodes(self.patterns) + self.expandGroupsUnlinkClones(self.patterns, True, True) + self.objectsToPaths(self.patterns) + del self.selected[id] + + self.skeletons=self.selected + self.expandGroupsUnlinkClones(self.skeletons, True, False) + self.objectsToPaths(self.skeletons) + + def lengthtotime(self,l): + ''' + Recieves an arc length l, and returns the index of the segment in self.skelcomp + containing the coresponding point, to gether with the position of the point on this segment. + + If the deformer is closed, do computations modulo the toal length. + ''' + if self.skelcompIsClosed: + l=l % sum(self.lengths) + if l<=0: + return 0,l/self.lengths[0] + i=0 + while (i----> TODO: really test if path is closed! end point==start point is not enough! + self.skelcompIsClosed = (self.skelcomp[0]==self.skelcomp[-1]) + + length=sum(self.lengths) + xoffset=self.skelcomp[0][0]-bbox[0]+self.options.toffset + yoffset=self.skelcomp[0][1]-(bbox[2]+bbox[3])/2-self.options.noffset + + if self.options.repeat: + NbCopies=max(1,int(round((length+self.options.space)/dx))) + width=dx*NbCopies + if not self.skelcompIsClosed: + width-=self.options.space + bbox=bbox[0],bbox[0]+width,bbox[2],bbox[3] + new=[] + for sub in p: + for i in range(0,NbCopies,1): + new.append(copy.deepcopy(sub)) + offset(sub,dx,0) + p=new + + for sub in p: + offset(sub,xoffset,yoffset) + + if self.options.stretch: + for sub in p: + stretch(sub,length/width,1,self.skelcomp[0]) + + for sub in p: + for ctlpt in sub: + self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2])) + + if self.options.vertical: + flipxy(p) + newp+=p + + d.value = cubicsuperpath.formatPath(newp) + +e = PathAlongPath() +e.affect() + + diff --git a/share/extensions/pathmodifier.py b/share/extensions/pathmodifier.py new file mode 100644 index 000000000..a27c547c1 --- /dev/null +++ b/share/extensions/pathmodifier.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python +''' +Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +barraud@math.univ-lille1.fr +''' +import inkex, cubicsuperpath, bezmisc, simplestyle +import copy, math, re, random, xml.xpath + +def parseTransform(transf,mat=[[1.0,0.0,0.0],[0.0,1.0,0.0]]): + if transf=="": + return(mat) + result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\(([^)]*)\)",transf) +#-- translate -- + if result.group(1)=="translate": + args=result.group(2).split(",") + dx=float(args[0]) + if len(args)==1: + dy=0.0 + else: + dy=float(args[1]) + matrix=[[1,0,dx],[0,1,dy]] +#-- scale -- + if result.groups(1)=="scale": + args=result.group(2).split(",") + sx=float(args[0]) + if len(args)==1: + sy=sx + else: + sy=float(args[1]) + matrix=[[sx,0,0],[0,sy,0]] +#-- rotate -- + if result.groups(1)=="rotate": + args=result.group(2).split(",") + a=float(args[0])*math.pi/180 + if len(args)==1: + cx,cy=(0.0,0.0) + else: + cx,cy=args[1:] + matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]] +#-- skewX -- + if result.groups(1)=="skewX": + a=float(result.group(2))*math.pi/180 + matrix=[[1,math.tan(a),0],[0,1,0]] +#-- skewX -- + if result.groups(1)=="skewX": + a=float(result.group(2))*math.pi/180 + matrix=[[1,0,0],[math.tan(a),1,0]] +#-- matrix -- + if result.group(1)=="matrix": + a11,a21,a12,a22,v1,v2=result.group(2).split(",") + matrix=[[float(a11),float(a12),float(v1)],[float(a21),float(a22),float(v2)]] + + matrix=composeTransform(mat,matrix) + if result.end() should it be given an id? + #seems to work without this!?! + clone.setAttributeNS(None,"id", self.uniqueId(node.tagName)) + node.parentNode.appendChild(clone) + clones[clone.getAttributeNS(None,"id")]=clone + return(clones) + + def uniqueId(self, prefix): + id="%s%04i"%(prefix,random.randint(0,9999)) + while len(xml.xpath.Evaluate('//*[@id="%s"]' % id,self.document)): + id="%s%04i"%(prefix,random.randint(0,9999)) + return(id) + + def expandGroups(self,aList,transferTransform=True): + for id, node in aList.items(): + if node.tagName == 'g': + mat=parseTransform(node.getAttributeNS(None,"transform")) + for child in node.childNodes: + if child.nodeType==child.ELEMENT_NODE: + if transferTransform: + applyTransformToNode(mat,child) + aList.update(self.expandGroups({child.getAttribute('id'):child})) + if transferTransform: + node.removeAttribute("transform") + del aList[id] + return(aList) + + def expandGroupsUnlinkClones(self,aList,transferTransform=True,doReplace=True): + for id in aList.keys()[:]: + node=aList[id] + if node.tagName == 'g': + self.expandGroups(aList,transferTransform) + self.expandGroupsUnlinkClones(aList,transferTransform,doReplace) + #Hum... not very efficient if there are many clones of groups... + elif node.tagName == 'use': + refid=node.getAttributeNS(inkex.NSS[u'xlink'],'href') + path = '//*[@id="%s"]' % refid[1:] + refnode = xml.xpath.Evaluate(path,self.document)[0] + newnode=refnode.cloneNode(True) + self.recursNewIds(newnode) + + if node.hasAttributeNS(None,u'style'): + style=simplestyle.parseStyle(node.getAttributeNS(None,u'style')) + refstyle=simplestyle.parseStyle(refnode.getAttributeNS(None,u'style')) + style.update(refstyle) + newnode.setAttributeNS(None,'style',simplestyle.formatStyle(style)) + applyTransformToNode(parseTransform(node.getAttributeNS(None,'transform')),newnode) + if doReplace: + parent=node.parentNode + parent.insertBefore(newnode,node) + parent.removeChild(node) + del aList[id] + newid=newnode.getAttributeNS(None,'id') + aList.update(self.expandGroupsUnlinkClones({newid:newnode},transferTransform,doReplace)) + return aList + + def recursNewIds(self,node): + if node.nodeType==node.ELEMENT_NODE and node.hasAttributeNS(None,u'id'): + node.setAttributeNS(None,u'id',self.uniqueId(node.tagName)) + for child in node.childNodes: + self.recursNewIds(child) + + + + +# def makeClonesReal(self,aList,doReplace=True,recursivelytransferTransform=True): +# for id in aList.keys(): +# node=aList[id] +# if node.tagName == 'g': +# childs={} +# for child in node.childNodes: +# if child.nodeType==child.ELEMENT_NODE: +# childid=child.getAttributeNS(None,'id') +# del aList[childid] +# aList.update(self.makeClonesReal({childid:child},doReplace)) +# elif node.tagName == 'use': +# refid=node.getAttributeNS(inkex.NSS[u'xlink'],'href') +# path = '//*[@id="%s"]' % refid[1:] +# refnode = xml.xpath.Evaluate(path,document)[0] +# clone=refnode.cloneNode(True) +# cloneid=self.uniqueId(clone.tagName) +# clone.setAttributeNS(None,'id', cloneid) +# style=simplestyle.parseStyle(node.getAttributeNS(None,u'style')) +# refstyle=simplestyle.parseStyle(refnode.getAttributeNS(None,u'style')) +# style.update(refstyle) +# clone.setAttributeNS(None,'style',simplestyle.formatStyle(style)) +# applyTransformToNode(parseTransform(node.getAttributeNS(None,'transform')),clone) +# if doReplace: +# parent=node.parentNode +# parent.insertBefore(clone,node) +# parent.removeChild(node) +# del aList[id] +# aList.update(self.expandGroupsUnlinkClones({cloneid:clone})) +# return aList + +################################ +#-- Object conversion ---------- +################################ + + def rectToPath(self,node,doReplace=True): + if node.tagName == 'rect': + x =float(node.getAttributeNS(None,u'x')) + y =float(node.getAttributeNS(None,u'y')) + try: + rx=float(node.getAttributeNS(None,u'rx')) + ry=float(node.getAttributeNS(None,u'ry')) + except: + rx=0 + ry=0 + w =float(node.getAttributeNS(None,u'width' )) + h =float(node.getAttributeNS(None,u'height')) + d ='M %f,%f '%(x+rx,y) + d+='L %f,%f '%(x+w-rx,y) + d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w,y+ry) + d+='L %f,%f '%(x+w,y+h-ry) + d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+w-rx,y+h) + d+='L %f,%f '%(x+rx,y+h) + d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x,y+h-ry) + d+='L %f,%f '%(x,y+ry) + d+='A %f,%f,%i,%i,%i,%f,%f '%(rx,ry,0,0,1,x+rx,y) + + newnode=self.document.createElement('path') + newnode.setAttributeNS(None,'d',d) + newnode.setAttributeNS(None,'id', self.uniqueId('path')) + newnode.setAttributeNS(None,'style',node.getAttributeNS(None,u'style')) + newnode.setAttributeNS(None,'transform',node.getAttributeNS(None,u'transform')) + fuseTransform(newnode) + if doReplace: + parent=node.parentNode + parent.insertBefore(newnode,node) + parent.removeChild(node) + return newnode + + def objectToPath(self,node,doReplace=True): + #--TODO: support other object types!!!! + #--TODO: make sure cubicsuperpath supports A and Q commands... + if node.tagName == 'rect': + return(self.rectToPath(node,doReplace)) + elif node.tagName == 'path': + attributes = node.attributes.keys() + for uri,attName in attributes: + if uri in [inkex.NSS[u'sodipodi'],inkex.NSS[u'inkscape']]: +# if attName not in ["d","id","style","transform"]: + node.removeAttributeNS(uri,attName) + fuseTransform(node) + return node + else: + inkex.debug("Please first convert objects to paths!...(got '%s')"%node.tagName) + return None + + def objectsToPaths(self,aList,doReplace=True): + newSelection={} + for id,node in aList.items(): + newnode=self.objectToPath(node,self.document) + del aList[id] + aList[newnode.getAttributeNS(None,u'id')]=newnode + + +################################ +#-- Action ---------- +################################ + + #-- overwrite this method in subclasses... + def effect(self): + #self.duplicateNodes(self.selected) + self.expandGroupsUnlinkClones(self.selected, True) + self.objectsToPaths(self.selected, True) + self.bbox=self.computeBBox(self.selected) + for id, node in self.selected.iteritems(): + if node.tagName == 'path': + d = node.attributes.getNamedItem('d') + p = cubicsuperpath.parsePath(d.value) + + #do what ever you want with p! + + d.value = cubicsuperpath.formatPath(p) + + +class Diffeo(PathModifier): + def __init__(self): + inkex.Effect.__init__(self) + + def applyDiffeo(self,bpt,vects=()): + ''' + bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt. + Defaults to identity! + ''' + for v in vects: + v[0]-=bpt[0] + v[1]-=bpt[1] + + #-- your transformations go here: + #x,y=bpt + #bpt[0]=f(x,y) + #bpt[1]=g(x,y) + #for v in vects: + # vx,vy=v + # v[0]=df/dx(x,y)*vx+df/dy(x,y)*vy + # v[1]=dg/dx(x,y)*vx+dg/dy(x,y)*vy + # + #-- !caution! y-axis is pointing downward! + + for v in vects: + v[0]+=bpt[0] + v[1]+=bpt[1] + + + def effect(self): + #self.duplicateNodes(self.selected) + self.expandGroupsUnlinkClones(self.selected, True) + self.expandGroups(self.selected, True) + self.objectsToPaths(self.selected, True) + self.bbox=self.computeBBox(self.selected) + for id, node in self.selected.iteritems(): + if node.tagName == 'path': + d = node.attributes.getNamedItem('d') + p = cubicsuperpath.parsePath(d.value) + + for sub in p: + for ctlpt in sub: + self.applyDiffeo(ctlpt[1],(ctlpt[0],ctlpt[2])) + + d.value = cubicsuperpath.formatPath(p) + +#e = Diffeo() +#e.affect() + + diff --git a/share/extensions/rubberstretch.py b/share/extensions/rubberstretch.py new file mode 100644 index 000000000..ae52d1c84 --- /dev/null +++ b/share/extensions/rubberstretch.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +''' +Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +barraud@math.univ-lille1.fr + +''' + +import inkex, cubicsuperpath, bezmisc, pathmodifier +import copy, math, re, random, xml.xpath + +class RubberStretch(pathmodifier.Diffeo): + def __init__(self): + pathmodifier.Diffeo.__init__(self) + self.OptionParser.add_option("-r", "--ratio", + action="store", type="float", + dest="ratio", default=0.5) + self.OptionParser.add_option("-c", "--curve", + action="store", type="float", + dest="curve", default=0.5) + + def applyDiffeo(self,bpt,vects=()): + for v in vects: + v[0]-=bpt[0] + v[1]-=bpt[1] + v[1]*=-1 + bpt[1]*=-1 + a=self.options.ratio/100 + b=min(self.options.curve/100,0.99) + x0= (self.bbox[0]+self.bbox[1])/2 + y0=-(self.bbox[2]+self.bbox[3])/2 + w,h=(self.bbox[1]-self.bbox[0])/2,(self.bbox[3]-self.bbox[2])/2 + + x,y=(bpt[0]-x0),(bpt[1]-y0) + sx=(1+b*(x/w+1)*(x/w-1))*2**(-a) + sy=(1+b*(y/h+1)*(y/h-1))*2**(-a) + bpt[0]=x0+x*sy + bpt[1]=y0+y/sx + for v in vects: + dx,dy=v + dXdx=sy + dXdy= x*2*b*y/h/h*2**(-a) + dYdx=-y*2*b*x/w/w*2**(-a)/sx/sx + dYdy=1/sx + v[0]=dXdx*dx+dXdy*dy + v[1]=dYdx*dx+dYdy*dy + + #--spherify +# s=((x*x+y*y)/(w*w+h*h))**(-a/2) +# bpt[0]=x0+s*x +# bpt[1]=y0+s*y +# for v in vects: +# dx,dy=v +# v[0]=(1-a/2/(x*x+y*y)*2*x*x)*s*dx+( -a/2/(x*x+y*y)*2*y*x)*s*dy +# v[1]=( -a/2/(x*x+y*y)*2*x*y)*s*dx+(1-a/2/(x*x+y*y)*2*y*y)*s*dy + + for v in vects: + v[0]+=bpt[0] + v[1]+=bpt[1] + v[1]*=-1 + bpt[1]*=-1 + +e = RubberStretch() +e.affect() + + -- 2.30.2