#!/usr/bin/env python3 # eggbot_sineandlace.py # # Generate sinusoidal and "lace" curves. The curves are described in SVG # along with the data necessary to regenerate them. The same data can be # used to generate new curves which are bounded by a pair of previously # generated curves. # Written by Daniel C. Newman for the Eggbot Project # dan newman @ mtbaldy us # Last updated 28 November 2010 # 15 October 2010 # 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 from math import pi, cos, sin from lxml import etree import inkex from inkex.paths import Path VERSION = 1 def parseDesc(str): """ Create a dictionary from string description """ if str is None: return {} else: return dict([tok.split(':') for tok in str.split(';') if len(tok)]) def formatDesc(d): """ Format an inline name1:value1;name2:value2;... style attribute value from a dictionary """ return ';'.join([atr + ':' + str(val) for atr, val in d.iteritems()]) def drawSine(cycles=8, rn=0, rm=0, nPoints=50, offset=None, height=200, width=3200, rescale=0.98, bound1='', bound2='', fun='sine', spline=True): """ cycles Number of periods to plot within the rectangle of width 'width' rn, rm Start the function (on the left edge) at the value x = 2 * pi * rn / rm. When rm = 0, function is started at x = 0. nPoints The number of points to sample the function at. Since the function is approximated using Bezier cubic splines, this isn't the number of points to plot. offset (x, y) coordinate of the lower left corner of the bounding rectangle in which to plot the function. height, width The height and width of the rectangle in which to plot the function. Ignored when bounding functions, bound1 and bound2, are supplied. rescale Multiplicative Y-scaling factor by which to rescale the plotted function so that it does not fully reach the vertical limits of its bounds. This aids in Eggbot plots by preventing lines from touching and overlapping. bound1, bound2 Descriptions of upper and lower bounding functions by which to limit the vertical range of the function to be plotted. fun May be either 'sine' or 'lace'. """ """ A complicated way of plotting y = sin(x) Complicated because we wish to generate the sine wave using a parametric representation. For plotting a single sine wave in Cartesian coordinates, this is overkill. However, it's useful for when we want to compress and expand the amplitude of the sine wave in accord with upper and lower boundaries which themselves are functions. By parameterizing everything in sight with the same parameter s and restricting s to the range [0, 1], our life is made much easier. """ if offset is None: offset = [0, 0] bounded = False if bound1 and bound2: func = parseDesc(bound1) if len(func) == 0: return None, None m1 = int(func['rm']) if m1 == 0: x_min1 = 0.0 else: x_min1 = 2 * pi * float(func['rn']) / float(m1) x_max1 = x_min1 + 2 * pi * float(func['cycles']) y_min1 = -1.0 y_max1 = 1.0 y_scale1 = float(func['height']) / (y_max1 - y_min1) y_offset1 = float(func['y']) Y1s = lambda s: y_offset1 - y_scale1 * sin(x_min1 + (x_max1 - x_min1) * s) func = parseDesc(bound2) if len(func) == 0: return None, None m2 = int(func['rm']) if m2 == 0: x_min2 = 0.0 else: x_min2 = 2 * pi * float(func['rn']) / float(m2) x_max2 = x_min2 + 2 * pi * float(func['cycles']) y_min2 = -1.0 y_max2 = 1.0 y_scale2 = float(func['height']) / (y_max2 - y_min2) y_offset2 = float(func['y']) Y2s = lambda s: y_offset2 - y_scale2 * sin(x_min2 + (x_max2 - x_min2) * s) bounded = True rescale = float(rescale) x_offset = float(offset[0]) y_offset = float(offset[1]) # Each cycle is 2pi r = 2 * pi * float(cycles) if (int(rm) == 0) or (int(rn) == 0): x_min = 0.0 else: x_min = 2 * pi * float(rn) / float(rm) x_max = x_min + r x_scale = float(width) / r # width / ( x_max - x_min ) y_min = -1.0 y_max = 1.0 y_scale = float(height) / (y_max - y_min) # Our parametric equations which map the results to our drawing window # Note the "-y_scale" that's because in SVG, the y-axis runs "backwards" if not fun: fun = 'sine' fun = fun.lower() if fun == 'sine': Xs = lambda s: x_offset + x_scale * (x_max - x_min) * s Ys = lambda s: y_offset - y_scale * sin(x_min + (x_max - x_min) * s) dYdXs = lambda s: -y_scale * cos(x_min + (x_max - x_min) * s) / x_scale elif fun == 'lace': Xs = lambda s: x_offset + x_scale * ((x_max - x_min) * s + 2 * sin(2 * s * (x_max - x_min) + pi)) dXs = lambda s: x_scale * (x_max - x_min) * (1.0 + 4.0 * cos(2 * s * (x_max - x_min) + pi)) Ys = lambda s: y_offset - y_scale * sin(x_min + (x_max - x_min) * s) dYs = lambda s: -y_scale * cos(x_min + (x_max - x_min) * s) * (x_max - x_min) dYdXs = lambda s: dYs(s) / dXs(s) else: inkex.errormsg('Unknown function {0} specified'.format(fun)) return # Derivatives: remember the chain rule.... # dXs = lambda s: x_scale * ( x_max - x_min ) # dYs = lambda s: -y_scale * cos( x_min + ( x_max - x_min ) * s ) * ( x_max - x_min ) # x_third is 1/3 the step size nPoints = int(nPoints) # x_third is 1/3 the step size; note that Xs(1) - Xs(0) = x_scale * ( x_max - x_min ) x_third = (Xs(1.0) - Xs(0.0)) / float(3 * (nPoints - 1)) if bounded: y_upper = Y2s(0.0) y_lower = Y1s(0.0) y_offset = 0.5 * (y_upper + y_lower) y_upper = y_offset + rescale * (y_upper - y_offset) y_lower = y_offset + rescale * (y_lower - y_offset) y_scale = (y_upper - y_lower) / (y_max - y_min) x1 = Xs(0.0) y1 = Ys(0.0) dx1 = 1.0 dy1 = dYdXs(0.0) # Starting point in the path is ( x, sin(x) ) path_data = [] path_data.append(['M', [x1, y1]]) for i in range(1, nPoints): s = float(i) / float(nPoints - 1) if bounded: y_upper = Y2s(s) y_lower = Y1s(s) y_offset = 0.5 * (y_upper + y_lower) y_upper = y_offset + rescale * (y_upper - y_offset) y_lower = y_offset + rescale * (y_lower - y_offset) y_scale = (y_upper - y_lower) / (y_max - y_min) x2 = Xs(s) y2 = Ys(s) dx2 = 1.0 dy2 = dYdXs(s) if dy2 > 10.0: dy2 = 10.0 elif dy2 < -10.0: dy2 = -10.0 # Add another segment to the plot if spline: path_data.append(['C', [x1 + (dx1 * x_third), y1 + (dy1 * x_third), x2 - (dx2 * x_third), y2 - (dy2 * x_third), x2, y2]]) else: path_data.append(['L', [x1, y1]]) path_data.append(['L', [x2, y2]]) x1 = x2 y1 = y2 dx1 = dx2 dy1 = dy2 path_desc = \ 'version:{0:d};style:linear;function:sin(x);'.format(VERSION) + \ 'cycles:{0:f};rn:{1:d};rm:{2:d};points:{3:d};'.format(cycles, rn, rm, nPoints) + \ 'width:{0:d};height:{1:d};x:{2:d};y:{3:d}'.format(width, height, offset[0], offset[1]) return path_data, path_desc class SineAndLace(inkex.EffectExtension): nsURI = 'http://sample.com/ns' nsPrefix = 'doof' def add_arguments(self, pars): pars.add_argument("--tab", help="The active tab when Apply was pressed") pars.add_argument('--fCycles', type=float, default=10.0, help='Number of cycles (periods)') pars.add_argument('--nrN', type=int, default=0, help='Start x at 2 * pi * n / m') pars.add_argument('--nrM', type=int, default=0, help='Start x at 2 * pi * n / m') pars.add_argument('--fRecess', type=float, default=2.0, help='Recede from envelope by factor') pars.add_argument("--nSamples", type=int, default=50.0, help="Number of points to sample") pars.add_argument("--nWidth", type=int, default=3200, help="Width in pixels") pars.add_argument("--nHeight", type=int, default=100, help="Height in pixels") pars.add_argument("--nOffsetX", type=int, default=0, help="Starting x coordinate (pixels)") pars.add_argument("--nOffsetY", type=int, default=400, help="Starting y coordinate (pixels)") pars.add_argument('--bLace', type=inkex.Boolean, default=False, help='Lace') pars.add_argument('--bSpline', type=inkex.Boolean, default=True, help='Spline') self.recess = 0.95 def effect(self): inkex.NSS[self.nsPrefix] = self.nsURI if self.options.bLace: func = 'lace' else: func = 'sine' f_recess = 1.0 if self.options.fRecess > 0.0: f_recess = 1.0 - self.options.fRecess / 100.0 if f_recess <= 0.0: f_recess = 0.0 if self.options.ids: if len(self.options.ids) == 2: desc1 = self.selected[self.options.ids[0]].get(inkex.addNS('desc', self.nsPrefix)) desc2 = self.selected[self.options.ids[1]].get(inkex.addNS('desc', self.nsPrefix)) if (not desc1) or (not desc2): inkex.errormsg('Selected objects do not smell right') return path_data, path_desc = drawSine(self.options.fCycles, self.options.nrN, self.options.nrM, self.options.nSamples, [self.options.nOffsetX, self.options.nOffsetY], self.options.nHeight, self.options.nWidth, f_recess, desc1, desc2, func, self.options.bSpline) else: inkex.errormsg('Exactly two objects must be selected') return else: self.document.getroot().set(inkex.addNS(self.nsPrefix, 'xmlns'), self.nsURI) path_data, path_desc = drawSine(self.options.fCycles, self.options.nrN, self.options.nrM, self.options.nSamples, [self.options.nOffsetX, self.options.nOffsetY], self.options.nHeight, self.options.nWidth, f_recess, '', '', func, self.options.bSpline) style = {'stroke': 'black', 'stroke-width': '1', 'fill': 'none'} path_attrs = { 'style': str(inkex.Style(style)), 'd': str(Path(path_data)), inkex.addNS('desc', self.nsPrefix): path_desc} newpath = etree.SubElement(self.document.getroot(), inkex.addNS('path', 'svg'), path_attrs, nsmap=inkex.NSS) if __name__ == '__main__': SineAndLace().run()