252 lines
14 KiB
Python
252 lines
14 KiB
Python
|
#!/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("--tab", type=str, dest="tab", default="object")
|
||
|
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')
|
||
|
|
||
|
so.tab = so.tab.replace('"', '') # removes de exceeding double quotes from the string
|
||
|
|
||
|
# 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()
|