several fixes and more extensions from 1.1 back again
This commit is contained in:
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
21
extensions/fablabchemnitz/spirograph/meta.json
Normal file
21
extensions/fablabchemnitz/spirograph/meta.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
30
extensions/fablabchemnitz/spirograph/spirograph.inx
Normal file
30
extensions/fablabchemnitz/spirograph/spirograph.inx
Normal 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>
|
248
extensions/fablabchemnitz/spirograph/spirograph.py
Normal file
248
extensions/fablabchemnitz/spirograph/spirograph.py
Normal 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()
|
Reference in New Issue
Block a user