several fixes and more extensions from 1.1 back again

This commit is contained in:
2022-09-02 03:15:14 +02:00
parent 1f3e8b9cb5
commit a38a160484
74 changed files with 11200 additions and 3 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
[
{
"name": "SpiroGraph",
"id": "fablabchemnitz.de.spirograph",
"path": "spirograph",
"dependent_extensions": null,
"original_name": "SpiroGraph",
"original_id": "fsmMLK.spiroGraph",
"license": "GNU GPL v3",
"license_url": "https://github.com/fsmMLK/inkscapeSpirograph/blob/main/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/spirograph",
"fork_url": "https://github.com/fsmMLK/inkscapeSpirograph",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/SpiroGraph",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/fsmMLK",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>SpiroGraph</name>
<id>fablabchemnitz.de.spirograph</id>
<param name="curveType" type="optiongroup" appearance="combo" gui-text="Curve type:">
<option value="Epitrochoid">Epitrochoid</option>
<option value="Hypotrochoid">Hypotrochoid</option>
</param>
<label appearance="header">Curve parameters</label>
<param name="radius_R" type="int" min="0" max="1000" gui-text="Fixed circle radius (R):">10</param>
<param name="radius_r" type="int" min="-1000" max="1000" gui-text="Rolling circle radius (r):">5</param>
<param name="pencil_distance" type="int" min="-1000" max="1000" gui-text="Pencil distance¹ (d):">2</param>
<label>¹ use d=r for Epi/Hypocycloid.</label>
<label appearance="header">Plot parameters</label>
<param name="detailLevel" type="int" min="1" max="10" gui-text="Detail level:">1</param>
<param name="drawBaseCircles" type="bool" gui-text="Draw base circles">false</param>
<param name="animate" type="bool" gui-text="Animate">false</param>
<param type="path" name="directory" gui-text="Animation directory:" mode="folder"/>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">spirograph.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,248 @@
#!/usr/bin/python
import math
import os
import numpy as np
import scipy.signal as scipySignal
import inkscapeMadeEasy.inkscapeMadeEasy_Base as inkBase
import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw
import inkscapeMadeEasy.inkscapeMadeEasy_Plot as inkPlot
# least common multiplier
def myLcm(x, y):
return x * y / math.gcd(int(x), int(y))
# ---------------------------------------------
# noinspection PyAttributeOutsideInit
class Spirograph(inkBase.inkscapeMadeEasy):
def __init__(self):
inkBase.inkscapeMadeEasy.__init__(self)
self.arg_parser.add_argument("--curveType", type=str, dest="curveType", default='resistor')
self.arg_parser.add_argument("--radius_R", type=float, dest="radius_R", default=10.0)
self.arg_parser.add_argument("--radius_r", type=float, dest="radius_r", default=5.0)
self.arg_parser.add_argument("--detailLevel", type=float, dest="detailLevel", default=1.0)
self.arg_parser.add_argument("--adaptiveTheta", type=self.bool, dest="adaptiveTheta", default=False)
self.arg_parser.add_argument("--pencil_distance", type=float, dest="pencil_distance", default=1.0)
self.arg_parser.add_argument("--drawBaseCircles", type=self.bool, dest="drawBaseCircles", default=False)
self.arg_parser.add_argument("--animate", type=self.bool, dest="animate", default=False)
self.arg_parser.add_argument("--directory", type=str, dest="directory", default='./')
def effect(self):
so = self.options
# sets the position to the viewport center, round to next 10.
position = [self.svg.namedview.center[0], self.svg.namedview.center[1]]
position[0] = int(math.ceil(position[0] / 10.0)) * 10
position[1] = int(math.ceil(position[1] / 10.0)) * 10
root_layer = self.document.getroot()
group = self.createGroup(root_layer, 'Spiro')
# curve parameters
R = so.radius_R
r = so.radius_r
d = so.pencil_distance
finalTheta = 2 * np.pi * myLcm(abs(r), R) / R
if 'hypo' in so.curveType.lower():
typeCurve = 'hypo'
if 'epi' in so.curveType.lower():
typeCurve = 'epi'
# markers and linestyles
Lgray = inkDraw.color.gray(0.8)
Dgray = inkDraw.color.gray(0.3)
# wheel
markerCenterDisk = inkDraw.marker.createDotMarker(self, nameID='diskCenter', scale=0.3, RenameMode=1, strokeColor=Dgray,
fillColor=inkDraw.color.defined('white'))
markerPen = inkDraw.marker.createDotMarker(self, nameID='diskPen', scale=0.3, RenameMode=1, strokeColor=Dgray,
fillColor=inkDraw.color.defined('white'))
[startArrowMarker, endArrowMarker] = inkDraw.marker.createArrow1Marker(self, nameID='arrowRot', RenameMode=1, scale=0.3, strokeColor=Dgray,
fillColor=Dgray)
if typeCurve == 'hypo':
self.lineStyleArrow = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=Dgray, markerStart=startArrowMarker, markerEnd=None)
else:
self.lineStyleArrow = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=Dgray, markerStart=None, markerEnd=endArrowMarker)
self.lineStyleARM = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=Dgray, markerStart=markerCenterDisk, markerEnd=markerPen)
self.lineStyleDisk = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=None, fillColor=Lgray)
# curve
self.lineStyleCurve = inkDraw.lineStyle.set(lineWidth=0.8, lineColor=inkDraw.color.defined('red'), markerStart=None, markerEnd=None,
markerMid=None)
self.lineStyleCurve2 = inkDraw.lineStyle.set(lineWidth=0.8, lineColor=inkDraw.color.defined('Dgreen'), markerStart=None, markerEnd=None,
markerMid=None)
self.lineStyleCurve3 = inkDraw.lineStyle.set(lineWidth=0.8, lineColor=inkDraw.color.defined('blue'), markerStart=None, markerEnd=None,
markerMid=None)
self.lineStylePre = inkDraw.lineStyle.set(lineWidth=1, lineColor=inkDraw.color.defined('red'))
self.constructionLine = inkDraw.lineStyle.set(lineWidth=0.5, lineColor=Dgray)
# draft Points
if so.adaptiveTheta:
nPrePoints = 10 * so.detailLevel # number of pre points per turn
thetasDraft = np.linspace(0, finalTheta, int(nPrePoints * finalTheta / (2 * np.pi)))
[pointsDraft, _, curvatureDraft] = self.calcCurve__trochoid(typeCurve, R, r, d, thetasDraft)
# find sampling points based on local curvature
nSamples = np.ones(curvatureDraft.shape)*min(2,so.detailLevel)
detailFactor=5
# treshold normalized curvatures
nSamples[curvatureDraft>0.8] *=detailFactor
detailFactor = 2.5
# check if vector changed direction abuptly
for i,p in enumerate(pointsDraft):
if i==0:
v1=pointsDraft[i+1]-pointsDraft[i]
v2=pointsDraft[i]-pointsDraft[-1]
elif i < len(pointsDraft)-1:
v1=pointsDraft[i+1]-pointsDraft[i]
v2=pointsDraft[i]-pointsDraft[i-1]
else:
v1=pointsDraft[0]-pointsDraft[i]
v2=pointsDraft[i]-pointsDraft[i-1]
v1=v1/np.linalg.norm(v1)
v2=v2/np.linalg.norm(v2)
if np.dot(v1,v2)<0.5:
nSamples[i] *=detailFactor
thetasFinal = np.array([])
for i in range(len(nSamples) - 1):
thetasFinal = np.append(thetasFinal, np.linspace(thetasDraft[i], thetasDraft[i + 1], int(nSamples[i]), endpoint=False))
thetasFinal = np.append(thetasFinal, finalTheta)
# filter the sampled angles to have a smooth transition.
Ntaps = 5
gaussWindow = scipySignal.gaussian(Ntaps, std=5)
gaussWindow = gaussWindow / np.sum(gaussWindow)
# inkPlot.plot.cartesian(self, root_layer, np.arange(thetasFinal.shape[0]), thetasFinal * 180 / np.pi, position, xTicks=False, yTicks=True, xTickStep=thetasFinal.shape[0]/10, yTickStep=120.0, xScale=10, yScale=10,xGrid=True, yGrid=True, forceXlim=None, forceYlim=None)
thetasFinal = scipySignal.filtfilt(gaussWindow, np.array([1]), thetasFinal)
# inkPlot.plot.cartesian(self, root_layer, np.arange(thetasFinal.shape[0]), thetasFinal * 180 / np.pi, position, xTicks=False, yTicks=True,xTickStep=thetasFinal.shape[0]/10, yTickStep=120.0, xScale=10, yScale=10, xGrid=True, yGrid=True, forceXlim=None, forceYlim=None,drawAxis=False)
else:
nPrePoints = 20 * so.detailLevel # number of pre points per turn
thetasFinal = np.linspace(0, finalTheta, int(nPrePoints * finalTheta / (2 * np.pi)))
# final shape
[PointsFinal, CentersFinal, curvatureFinal] = self.calcCurve__trochoid(typeCurve, R, r, d, thetasFinal)
[PointsFinal2, CentersFinal2, curvatureFinal2] = self.calcCurve__trochoid(typeCurve, R, r, -d, thetasFinal)
[PointsFinal3, CentersFinal3, curvatureFinal3] = self.calcCurve__trochoid(typeCurve, R, r, r, thetasFinal)
if so.animate:
animGroup = self.createGroup(group, 'Anim')
circle_R = inkDraw.circle.centerRadius(parent=animGroup, centerPoint=[0, 0], radius=R, offset=position, lineStyle=self.constructionLine)
# draw planetary wheel
wheelGroup = self.createGroup(animGroup, 'Anim')
circle_r = inkDraw.circle.centerRadius(wheelGroup, centerPoint=CentersFinal[0], radius=r, offset=position, lineStyle=self.lineStyleDisk)
arms1 = inkDraw.line.absCoords(wheelGroup, coordsList=[CentersFinal[0], PointsFinal[0]], offset=position, lineStyle=self.lineStyleARM)
arms2 = inkDraw.line.absCoords(wheelGroup, coordsList=[CentersFinal2[0], PointsFinal2[0]], offset=position, lineStyle=self.lineStyleARM)
arms3 = inkDraw.line.absCoords(wheelGroup, coordsList=[CentersFinal3[0], PointsFinal3[0]], offset=position, lineStyle=self.lineStyleARM)
arc1 = inkDraw.arc.centerAngStartAngEnd(wheelGroup, centerPoint=CentersFinal[0], radius=r * 0.6, angStart=40, angEnd=80, offset=position,
lineStyle=self.lineStyleArrow)
arc2 = inkDraw.arc.centerAngStartAngEnd(wheelGroup, centerPoint=CentersFinal[0], radius=r * 0.6, angStart=160, angEnd=200,
offset=position, lineStyle=self.lineStyleArrow)
arc3 = inkDraw.arc.centerAngStartAngEnd(wheelGroup, centerPoint=CentersFinal[0], radius=r * 0.6, angStart=280, angEnd=320,
offset=position, lineStyle=self.lineStyleArrow)
self.exportSVG(animGroup, os.path.join(so.directory,'outSVG_%1.5d.svg' % 0))
for i in range(1, len(thetasFinal)):
self.moveElement(wheelGroup, [CentersFinal[i][0] - CentersFinal[i - 1][0], CentersFinal[i][1] - CentersFinal[i - 1][1]])
if typeCurve == 'hypo':
self.rotateElement(wheelGroup, [position[0] + CentersFinal[i][0], position[1] + CentersFinal[i][1]],
(thetasFinal[i] - thetasFinal[i - 1]) * (R - r) / r * 180 / np.pi)
else:
self.rotateElement(wheelGroup, [position[0] + CentersFinal[i][0], position[1] + CentersFinal[i][1]],
- (thetasFinal[i] - thetasFinal[i - 1]) * (R + r) / r * 180 / np.pi)
curve1 = inkDraw.line.absCoords(parent=animGroup, coordsList=PointsFinal[:i + 1], offset=position, lineStyle=self.lineStyleCurve,
closePath=False)
curve2 = inkDraw.line.absCoords(parent=animGroup, coordsList=PointsFinal2[:i + 1], offset=position, lineStyle=self.lineStyleCurve2,
closePath=False)
curve3 = inkDraw.line.absCoords(parent=animGroup, coordsList=PointsFinal3[:i + 1], offset=position, lineStyle=self.lineStyleCurve3,
closePath=False)
self.exportSVG(animGroup, os.path.join(so.directory , 'outSVG_%1.5d.svg' % i))
self.removeElement(curve1)
self.removeElement(curve2)
self.removeElement(curve3)
self.removeElement(animGroup)
else:
if so.drawBaseCircles:
inkDraw.circle.centerRadius(parent=group, centerPoint=position, radius=R, offset=[0, 0], lineStyle=self.constructionLine)
if typeCurve == 'hypo':
inkDraw.circle.centerRadius(parent=group, centerPoint=position, radius=r, offset=[R - r, 0], lineStyle=self.constructionLine)
if typeCurve == 'epi':
inkDraw.circle.centerRadius(parent=group, centerPoint=position, radius=r, offset=[R + r, 0], lineStyle=self.constructionLine)
inkDraw.line.absCoords(group, PointsFinal, position, 'spiro', self.lineStyleCurve, closePath=True)
# plot curvatures
if False:
inkPlot.plot.polar(self, group, curvatureFinal, thetasFinal * 180 / np.pi, [position[0] + 3 * R, position[1]], rTicks=False,
tTicks=False, rTickStep=0.2, tTickStep=45.0, rScale=20, rGrid=True, tGrid=True, lineStylePlot=self.lineStyleCurve,
forceRlim=[0.0, 1.0])
return
# typeCurve: 'hypo', 'epi'
def calcCurve__trochoid(self, typeCurve, R, r, d, thetas):
j = complex(0, 1)
if typeCurve.lower() == 'hypo':
# https://www.mathcurve.com/courbes2d.gb/hypotrochoid/hypotrochoid.shtml
P_complex = (R - r) * np.exp(j * thetas) + d * np.exp(-j * thetas * (R - r) / r)
dP_complex = (R - r) * j * np.exp(j * thetas) + d * (-j) * (R - r) / r * np.exp(-j * thetas * (R - r) / r)
ddP_complex = (R - r) * (-1) * np.exp(j * thetas) + d * (-1) * ((R - r) / r) ** 2 * np.exp(-j * thetas * (R - r) / r)
centerGear = (R - r) * np.exp(j * thetas)
if typeCurve.lower() == 'epi':
# https://www.mathcurve.com/courbes2d.gb/epitrochoid/epitrochoid.shtml
P_complex = (R + r) * np.exp(j * thetas) - d * np.exp(j * thetas * (R + r) / r)
dP_complex = (R + r) * j * np.exp(j * thetas) - d * j * (R + r) / r * np.exp(j * thetas * (R + r) / r)
ddP_complex = (R + r) * (-1) * np.exp(j * thetas) - d * (-1) * ((R + r) / r) ** 2 * np.exp(j * thetas * (R + r) / r)
centerGear = (R + r) * np.exp(j * thetas)
with np.errstate(divide='ignore', invalid='ignore'):
curvature = np.divide(abs(dP_complex.real * ddP_complex.imag - dP_complex.imag * ddP_complex.real),
(dP_complex.real ** 2 + dP_complex.imag ** 2) ** (2 / 3))
# remove Nan=0/0
np.nan_to_num(curvature, copy=False)
# remove values too large
curvature[curvature > 10 * np.mean(curvature)] = 0.0
# self.Dump(curvature, '/home/fernando/lixo.txt', 'w')
# normalize curvature
curvature = self._normalizeCurvatures(curvature, 0.0, 1.0)
Points = np.column_stack((P_complex.real, P_complex.imag))
Centers = np.column_stack((centerGear.real, centerGear.imag))
return [Points, Centers, curvature]
def _normalizeCurvatures(self, curvatures, normMin=0.0, normMax=1.0):
y1 = normMin
y2 = normMax
x1 = np.min(curvatures)
x2 = np.max(curvatures)
alpha = (y2 - y1) / (x2 - x1)
return alpha * (curvatures - x1) + y1
if __name__ == '__main__':
sp = Spirograph()
sp.run()