Source code for kf.QtPlotter

"""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()
################################################################