Added Sine and Lace, reorganized imports of Twist

This commit is contained in:
leyghisbb 2020-07-31 01:34:36 +02:00
parent 7a022d5bca
commit 09cfb9b610
3 changed files with 384 additions and 3 deletions

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<_name>Sine and Lace</_name>
<id>fablabchemnitz.de.sineandlace</id>
<param name="tab" type="notebook">
<page name="splash" _gui-text="Sine and Lace">
<param name="nWidth" type="int" min="1" max="10000" _gui-text="Width (pixels)">3200</param>
<param name="nHeight" type="int" min="1" max="10000" _gui-text="Height (pixels)">100</param>
<param name="fCycles" type="float" min="0.0001" max="10000" precision="5" _gui-text="Number of cycles (periods)">10</param>
<param name="nrN" type="int" min="-100" max="100" _gui-text="Start angle at 2 pi ( n / m ); n = ">0</param>
<param name="nrM" type="int" min="-100" max="100" _gui-text="Start angle at 2 pi ( n / m ); m = ">0</param>
<param name="fRecess" type="float" min="0" max="100" precision="5" _gui-text="Recede from envelope by percentage">2</param>
<param name="nSamples" type="int" min="2" max="100000" _gui-text="Number of sample points">1000</param>
<param name="nOffsetX" type="int" min="-10000" max="10000" _gui-text="Starting x coordinate (pixels)">0</param>
<param name="nOffsetY" type="int" min="-10000" max="10000" _gui-text="Starting y coordinate (pixels)">500</param>
<param name="bLace" type="boolean" _gui-text="Lace">true</param>
<param name="bSpline" type="boolean" _gui-text="Spline">false</param>
</page>
<page name="info" _gui-text="About...">
<_param name="aboutpage" type="description" xml:space="preserve">
This extension renders sinusoidal and "lace"
patterns whose period is a specified multiple
of the document width or any specified width.
By selecting two previously drawn patterns,
a third pattern may be inscribed within them.
Patterns may not be inscribed within an inscribed
pattern, however.
This extension may be found at Thingiverse as
Thing #24594.
Sine and Lace v0.9
Dan Newman (dan newman @ mtbaldy us)
12 June 2012
</_param>
</page>
</param>
<effect>
<effects-menu>
<submenu _name="FabLab Chemnitz">
<submenu _name="Shape/Pattern from Generator" />
</submenu>
</effects-menu>
</effect>
<script>
<command reldir="extensions" interpreter="python">fablabchemnitz_eggbot_sineandlace.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,334 @@
#!/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.Effect):
nsURI = 'http://sample.com/ns'
nsPrefix = 'doof'
def __init__(self):
inkex.Effect.__init__(self)
self.arg_parser.add_argument("--tab", help="The active tab when Apply was pressed")
self.arg_parser.add_argument('--fCycles', type=float, default=10.0, help='Number of cycles (periods)')
self.arg_parser.add_argument('--nrN', type=int, default=0, help='Start x at 2 * pi * n / m')
self.arg_parser.add_argument('--nrM', type=int, default=0, help='Start x at 2 * pi * n / m')
self.arg_parser.add_argument('--fRecess', type=float, default=2.0, help='Recede from envelope by factor')
self.arg_parser.add_argument("--nSamples", type=int, default=50.0, help="Number of points to sample")
self.arg_parser.add_argument("--nWidth", type=int, default=3200, help="Width in pixels")
self.arg_parser.add_argument("--nHeight", type=int, default=100, help="Height in pixels")
self.arg_parser.add_argument("--nOffsetX", type=int, default=0, help="Starting x coordinate (pixels)")
self.arg_parser.add_argument("--nOffsetY", type=int, default=400, help="Starting y coordinate (pixels)")
self.arg_parser.add_argument('--bLace', type=inkex.Boolean, default=False, help='Lace')
self.arg_parser.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()

View File

@ -31,11 +31,10 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from inkex import bezier
import cspsubdiv
from inkex.paths import Path, CubicSuperPath
import inkex import inkex
from inkex import Transform from inkex import Transform
from inkex import bezier
from inkex.paths import Path, CubicSuperPath
from lxml import etree from lxml import etree
def subdivideCubicPath(sp, flat, i=1): def subdivideCubicPath(sp, flat, i=1):