"""PyQt5 widgets to render a 2D suspended plotter system based on winch-driven lines.
"""
################################################################
# Written in 2019 by Garth Zeglin <garthz@cmu.edu>
# To the extent possible under law, the author has dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.
# You should have received a copy of the CC0 Public Domain Dedication along with this software.
# If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
################################################################
import math, logging
# for documentation on the PyQt5 API, see http://pyqt.sourceforge.net/Docs/PyQt5/index.html
from PyQt5 import QtCore, QtGui, QtWidgets
# set up logger for module
log = logging.getLogger('QtPlotter')
# filter out most logging; the default is NOTSET which passes along everything
# log.setLevel(logging.WARNING)
################################################################
[docs]class QtPlotterCartoon(QtWidgets.QWidget):
"""Custom widget representing a single plotter as a 2D cartoon view."""
def __init__(self):
super().__init__()
self.setMinimumSize(QtCore.QSize(100, 100))
self.setAutoFillBackground(True)
# graphical state variables
self.positions = [0.0, 0.0] # units are microsteps
# Create a path representing the basic winch glyph. The default coordinates
# have +X to the right, +Y down. The drawing units are millimeters.
self.winch_symbol = QtGui.QPainterPath()
self.winch_symbol.addEllipse(QtCore.QPointF(0.0, 0.0), 20.0, 20.0) # center, rx, ry
self.winch_symbol.moveTo(-10.0, 0.0)
self.winch_symbol.lineTo( 10.0, 0.0)
self.winch_symbol.moveTo( 0.0, 10.0)
self.winch_symbol.lineTo( 0.0,-10.0)
# finish initialization
self.show()
return
[docs] def update_positions(self, positions):
self.positions = positions
self.repaint()
def _calculate_intersection(self, R1, R2):
# given line lengths R1 and R2 in millimeters, calculate the intersection of the circular loci
# returns (x, y) location or None
# the location is relative to the midpoint of the pulley points
# see math/suspended_point_kinematics.py for the derivation
D = 1600 # distance between winches in millimeters
x = (1/2)*R1**2/D - 1/2*R2**2/D
# y = -1/2*sqrt(-D**4 + 2*D**2*R1**2 + 2*D**2*R2**2 - R1**4 + 2*R1**2*R2**2 - R2**4)/D
yterm = -D**4 + 2*D**2*R1**2 + 2*D**2*R2**2 - R1**4 + 2*R1**2*R2**2 - R2**4
if yterm >= 0:
return x, math.sqrt(yterm) / (2*D)
else:
return None
def _draw_winch(self, qp, x, y, position):
qp.save()
qp.translate(QtCore.QPointF(x, y))
# The default coordinate system rotation uses +Z pointing into the
# screen; this changes sign so positive displacements are
# counter-clockwise, i.e. right-hand-rule on the vector pointing *out*
# of the screen. This rescales from microsteps to degrees.
qp.rotate(-position*(360/800))
# draw the winch symbol
pen = QtGui.QPen(QtCore.Qt.black)
pen.setWidthF(4)
qp.setPen(pen)
qp.drawPath(self.winch_symbol)
qp.restore()
return
[docs] def paintEvent(self, e):
geometry = self.geometry()
width = geometry.width()
height = geometry.height()
qp = QtGui.QPainter()
qp.begin(self)
qp.fillRect(QtCore.QRectF(0, 0, width, height), QtCore.Qt.white)
qp.setRenderHint(QtGui.QPainter.Antialiasing)
# Set up a coordinate system scaled to real-world millimeters, centered in the visible area.
qp.save()
scaling = width/2 if width < height else height/2
scaling *= 0.001 # minimum 2 meters visible in either direction
qp.translate(QtCore.QPointF(width/2, height/2))
qp.scale(scaling, scaling)
# draw the left and right winches
self._draw_winch(qp, -800.0, -800.0, self.positions[0])
self._draw_winch(qp, 800.0, -800.0, self.positions[1])
# Draw the left and right counterweights. The winch is 800 units/rev and has a circumference of (40 * pi) millimeters
linear_scaling = (40 * math.pi) / 800
y = [pos*linear_scaling for pos in self.positions]
y[0] = max(min(y[0], 750), -750)
y[1] = max(min(y[1], 750), -750)
pen = QtGui.QPen(QtCore.Qt.black)
pen.setWidthF(4)
qp.setPen(pen)
qp.drawLine(QtCore.QPointF(-820, -800), QtCore.QPointF(-820, y[0]))
qp.drawLine(QtCore.QPointF( 820, -800), QtCore.QPointF( 820, -y[1]))
qp.drawRect(QtCore.QRectF( -870, y[0], 100, 50))
qp.drawRect(QtCore.QRectF( 770, -y[1], 100, 50))
# Draw each locus of possible line endpoints as a circle
pen = QtGui.QPen(QtCore.Qt.black)
pen.setWidthF(3)
qp.setPen(pen)
neutral_radius = 1000.0
R1 = neutral_radius - y[0]
R2 = neutral_radius + y[1]
qp.drawEllipse(QtCore.QPointF(-800, -800), R1, R1)
qp.drawEllipse(QtCore.QPointF( 800, -800), R2, R2)
# Calculate the possible intersection of loci, and if defined, draw lines representing the control lines.
intersection = self._calculate_intersection(R1, R2)
if intersection is not None:
xi, yi = intersection
qp.drawLine(QtCore.QPointF(-800, -800), QtCore.QPointF(xi, yi - 800))
qp.drawLine(QtCore.QPointF( 800, -800), QtCore.QPointF(xi, yi - 800))
# restore the initial unscaled coordinates
qp.restore()
# draw the text annotations
qp.drawText(10, height-4, "%d, %d steps. %d, %d mm." % (int(self.positions[0]), int(self.positions[1]), -int(y[0]), int(y[1])))
qp.end()
################################################################