332 lines
12 KiB
Python
332 lines
12 KiB
Python
#!/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 SpiroSine(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__':
|
|
SpiroSine().run() |