diff --git a/examples/fractal.py b/examples/fractal.py index 55491080..05cd2409 100644 --- a/examples/fractal.py +++ b/examples/fractal.py @@ -56,7 +56,7 @@ def update(): p2 = pts[i+1] v2 = p2 - p1 t = p1 - pts[0] - r = v2.angle(v1) + r = v1.angle(v2) s = v2.length() / l1 trs.append(pg.SRTTransform({'pos': t, 'scale': (s, s), 'angle': r})) diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index bdd8bc33..973226dd 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -12,21 +12,23 @@ from math import atan2, hypot, degrees class Point(QtCore.QPointF): """Extension of QPointF which adds a few missing methods.""" + __slots__ = () + def __init__(self, *args): if len(args) == 1: - if isinstance(args[0], QtCore.QSizeF): - QtCore.QPointF.__init__(self, float(args[0].width()), float(args[0].height())) + if isinstance(args[0], (QtCore.QSize, QtCore.QSizeF)): + super().__init__(float(args[0].width()), float(args[0].height())) return - elif isinstance(args[0], float) or isinstance(args[0], int): - QtCore.QPointF.__init__(self, float(args[0]), float(args[0])) + elif isinstance(args[0], (int, float)): + super().__init__(float(args[0]), float(args[0])) return elif hasattr(args[0], '__getitem__'): - QtCore.QPointF.__init__(self, float(args[0][0]), float(args[0][1])) - return + super().__init__(float(args[0][0]), float(args[0][1])) + return elif len(args) == 2: - QtCore.QPointF.__init__(self, args[0], args[1]) + super().__init__(args[0], args[1]) return - QtCore.QPointF.__init__(self, *args) + super().__init__(*args) def __len__(self): return 2 @@ -41,6 +43,10 @@ class Point(QtCore.QPointF): return self.y() else: raise IndexError("Point has no index %s" % str(i)) + + def __iter__(self): + yield(self.x()) + yield(self.y()) def __setitem__(self, i, x): if i == 0: @@ -87,47 +93,67 @@ class Point(QtCore.QPointF): return self._math_('__pow__', a) def _math_(self, op, x): - x = Point(x) - return Point(getattr(self[0], op)(x[0]), getattr(self[1], op)(x[1])) + if not isinstance(x, QtCore.QPointF): + x = Point(x) + return Point(getattr(self.x(), op)(x.x()), getattr(self.y(), op)(x.y())) def length(self): """Returns the vector length of this Point.""" - return hypot(self[0], self[1]) # length + return hypot(self.x(), self.y()) # length def norm(self): """Returns a vector in the same direction with unit length.""" return self / self.length() - def angle(self, a): - """Returns the angle in degrees between this vector and the vector a.""" - rads = atan2(self.y(), self.x()) - atan2(a.y(), a.x()) + def angle(self, a, units="degrees"): + """ + Returns the angle in degrees between this vector and the vector a. + + Parameters + ---------- + a : Point, QPointF or QPoint + The Point to return the angle with + units : str, optional + The units with which to compute the angle with, "degrees" or "radians", + default "degrees" + + Returns + ------- + float + The angle between the two points + """ + rads = atan2(a.y(), a.x()) - atan2(self.y(), self.x()) + if units == "radians": + return rads return degrees(rads) def dot(self, a): """Returns the dot product of a and this Point.""" - a = Point(a) - return self[0]*a[0] + self[1]*a[1] + if not isinstance(a, QtCore.QPointF): + a = Point(a) + return Point.dotProduct(self, a) def cross(self, a): - a = Point(a) - return self[0]*a[1] - self[1]*a[0] + if not isinstance(a, QtCore.QPointF): + a = Point(a) + return self.x() * a.y() - self.y() * a.x() def proj(self, b): """Return the projection of this vector onto the vector b""" - b1 = b / b.length() + b1 = b.norm() return self.dot(b1) * b1 def __repr__(self): - return "Point(%f, %f)" % (self[0], self[1]) + return "Point(%f, %f)" % (self.x(), self.y()) def min(self): - return min(self[0], self[1]) + return min(self.x(), self.y()) def max(self): - return max(self[0], self[1]) + return max(self.x(), self.y()) def copy(self): return Point(self) def toQPoint(self): - return QtCore.QPoint(int(self[0]), int(self[1])) + return self.toPoint() diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index a340f8b6..e40fa40f 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -66,8 +66,7 @@ class SRTTransform(QtGui.QTransform): dp3 = Point(p3-p1) ## detect flipped axes - if dp2.angle(dp3) > 0: - #da = 180 + if dp3.angle(dp2, units="radians") > 0: da = 0 sy = -1.0 else: diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 273d419f..6a2ab51b 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -438,14 +438,11 @@ class GraphicsItem(object): """ if relativeItem is None: relativeItem = self.parentItem() - tr = self.itemTransform(relativeItem) if isinstance(tr, tuple): ## difference between pyside and pyqt tr = tr[0] - #vec = tr.map(Point(1,0)) - tr.map(Point(0,0)) vec = tr.map(QtCore.QLineF(0,0,1,0)) - #return Point(vec).angle(Point(1,0)) return vec.angleTo(QtCore.QLineF(vec.p1(), vec.p1()+QtCore.QPointF(1,0))) #def itemChange(self, change, value): diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 6c5f8418..b6e74095 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -17,7 +17,7 @@ import numpy as np #from numpy.linalg import norm from ..Point import Point from ..SRTTransform import SRTTransform -from math import atan2, cos, sin, hypot, radians +from math import atan2, cos, degrees, sin, hypot from .. import functions as fn from .GraphicsObject import GraphicsObject from .UIGraphicsItem import UIGraphicsItem @@ -936,7 +936,7 @@ class ROI(GraphicsObject): return ## determine new rotation angle, constrained if necessary - ang = newState['angle'] - lp0.angle(lp1) + ang = newState['angle'] - lp1.angle(lp0) if ang is None: ## this should never happen.. return if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): @@ -972,7 +972,7 @@ class ROI(GraphicsObject): except OverflowError: return - ang = newState['angle'] - lp0.angle(lp1) + ang = newState['angle'] - lp1.angle(lp0) if ang is None: return if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): @@ -1663,12 +1663,11 @@ class LineROI(ROI): pos2 = Point(pos2) d = pos2-pos1 l = d.length() - ang = Point(1, 0).angle(d) - ra = radians(ang if ang is not None else 0.) + ra = d.angle(Point(1, 0), units="radians") c = Point(-width/2. * sin(ra), -width/2. * cos(ra)) pos1 = pos1 + c - ROI.__init__(self, pos1, size=Point(l, width), angle=ang, **args) + ROI.__init__(self, pos1, size=Point(l, width), angle=degrees(ra), **args) self.addScaleRotateHandle([0, 0.5], [1, 0.5]) self.addScaleRotateHandle([1, 0.5], [0, 0.5]) self.addScaleHandle([0.5, 1], [0.5, 0.5]) @@ -2359,7 +2358,7 @@ class RulerROI(LineSegmentROI): vec = Point(h2) - Point(h1) length = vec.length() - angle = vec.angle(Point(1, 0)) + angle = Point(1, 0).angle(vec) pvec = p2 - p1 pvecT = Point(pvec.y(), -pvec.x()) diff --git a/pyqtgraph/tests/test_Point.py b/pyqtgraph/tests/test_Point.py new file mode 100644 index 00000000..a8252e11 --- /dev/null +++ b/pyqtgraph/tests/test_Point.py @@ -0,0 +1,63 @@ +import pytest +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore +import math + +angles = [ + ((1, 0), (0, 1), 90), + ((0, 1), (1, 0), -90), + ((-1, 0), (-1, 0), 0), + ((0, -1), (0, 1), 180), +] +@pytest.mark.parametrize("p1, p2, angle", angles) +def test_Point_angle(p1, p2, angle): + p1 = pg.Point(*p1) + p2 = pg.Point(*p2) + assert p1.angle(p2) == angle + + +inits = [ + (QtCore.QSizeF(1, 0), (1.0, 0.0)), + ((0, -1), (0.0, -1.0)), + ([1, 1], (1.0, 1.0)), +] +@pytest.mark.parametrize("initArgs, positions", inits) +def test_Point_init(initArgs, positions): + if isinstance(initArgs, QtCore.QSizeF): + point = pg.Point(initArgs) + else: + point = pg.Point(*initArgs) + assert (point.x(), point.y()) == positions + +lengths = [ + ((0, 1), 1), + ((1, 0), 1), + ((0, 0), 0), + ((1, 1), math.sqrt(2)), + ((-1, -1), math.sqrt(2)) +] +@pytest.mark.parametrize("initArgs, length", lengths) +def test_Point_length(initArgs, length): + point = pg.Point(initArgs) + assert point.length() == length + +min_max = [ + ((0, 1), 0, 1), + ((1, 0), 0, 1), + ((-math.inf, 0), -math.inf, 0), + ((0, math.inf), 0, math.inf) +] +@pytest.mark.parametrize("initArgs, min_, max_", min_max) +def test_Point_min_max(initArgs, min_, max_): + point = pg.Point(initArgs) + assert min(point) == min_ + assert max(point) == max_ + +projections = [ + ((0, 1), (1, 0), (1, 1)) +] +@pytest.mark.parametrize("p1_arg, p2_arg, projection", projections) +def test_Point_projection(p1_arg, p2_arg, projection): + p1 = pg.Point(p1_arg) + p2 = pg.Point(p2_arg) + p1.proj(p2) == projection \ No newline at end of file