Пример #1
0
    def center(self):
        """
        Returns a Point representing the center of the Triangle.
        
        >>> P = point.Point
        >>> print(Triangle(P(3, 5), P(0, 1), P(-1, 3)).center())
        (0.6666666666666667, 3.0)
        >>> print(Triangle(P(-10, 0), P(0, 0.000001), P(10, 0)).center())
        (0.0, 5e-07)
        """

        if self.isDegenerate():
            minX = min(self.p1.x, self.p2.x, self.p3.x)
            minY = min(self.p1.y, self.p2.y, self.p3.y)
            maxX = max(self.p1.x, self.p2.x, self.p3.x)
            maxY = max(self.p1.y, self.p2.y, self.p3.y)

            L = line.Line(point.Point(minX, minY), point.Point(maxX, maxY))

            return L.midpoint()

        Line = line.Line
        line12 = Line(self.p1, self.p2)
        line23 = Line(self.p2, self.p3)
        testLine1 = Line(self.p3, line12.midpoint())
        testLine2 = Line(self.p1, line23.midpoint())
        return testLine1.intersection(testLine2)
Пример #2
0
    def normalized(self, length=1):
        """
        Returns a new Line whose first point is the same as self's, but whose
        second point is moved such that the length of the new Line is as
        specified. The new Line's second point will be in the same direction
        relative to the first point as the original Line's was.
        
        If self is degenerate it cannot be normalized to a non-zero length, so
        a copy of self will be returned if length is zero and None will be
        returned otherwise.
        
        Specifying a length of zero will return a degenerate line both of whose
        points are the same as self.p1.
        
        >>> P = point.Point
        >>> print(Line(P(0, 0), P(10, 0)).normalized())
        Line from (0, 0) to (1.0, 0.0)
        >>> print(Line(P(0, 0), P(0, 10)).normalized())
        Line from (0, 0) to (0, 1)
        >>> print(Line(P(0, 0), P(0, -10)).normalized())
        Line from (0, 0) to (0, -1)
        >>> print(Line(P(1, -1), P(-2, -6)).normalized())
        Line from (1, -1) to (0.48550424457247343, -1.8574929257125443)
        >>> print(Line(P(0, 0), P(10, 0)).normalized(length=5))
        Line from (0, 0) to (5.0, 0.0)
        
        Degenerate cases:
        
        >>> LDeg = Line(P(2, -3), P(2, -3))
        >>> LDeg.normalized() is None
        True
        >>> LDeg.normalized(0) == LDeg
        True
        >>> LDeg.normalized(0) is LDeg
        False
        """

        p = self.p1

        if not length:
            return type(self)(p, p)

        slope = self.slope()

        if slope is None:
            return (None if length else self.__copy__())

        if abs(slope) == float("+inf"):
            # line is vertical
            sign = (-1 if self.p2.y < p.y else 1)
            return type(self)(p, point.Point(p.x, p.y + sign * length))

        sign = (-1 if self.p2.x < p.x else 1)
        x = p.x + sign * math.sqrt(length**2 / (1 + slope**2))
        y = p.y + slope * (x - p.x)
        return type(self)(p, point.Point(x, y))
Пример #3
0
    def corners(self):
        """
        Returns a tuple with the two corners as Points.
        
        >>> for obj in _testingValues[1].corners(): print(obj)
        (10, -20)
        (90, 100)
        """

        p1 = point.Point(self.xMin, self.yMin)
        p2 = point.Point(self.xMax, self.yMax)
        return (p1, p2)
Пример #4
0
 def transitions(self, xStart, xStop, yCoord, leftToRight):
     """
     Finds the intersection of self and the horizontal line segment defined
     by xStart, xStop, and yCoord, then for each point in the intersection
     determines whether the curve passes by left-to-right or right-to-left
     when looked at in the specified leftToRight orientation. See the
     doctest examples below for a sense of how this works; basically, a dict
     is returned with True mapping to seen left-to-right transitions, and
     False mapping to seen right-to-left transitions.
     
     >>> obj = _testingValues[0]
     >>> print(obj.transitions(0, 10, 3, True))
     {False: 1, True: 0}
     >>> print(obj.transitions(0, 10, 3, False))
     {False: 0, True: 1}
     
     >>> obj = _testingValues[4]
     >>> print(obj.transitions(0, 5, 2, True))
     {False: 1, True: 1}
     >>> print(obj.transitions(0, 5, 1, True))
     {False: 2, True: 1}
     """
     
     ray = type(self)(
       start = point.Point(xStart, yCoord),
       control0 = None,
       control1 = None,
       end = point.Point(xStop, yCoord))
     
     sects = self.intersection(ray)
     r = {False: 0, True: 0}
     CE = mathutilities.closeEnough
     
     for p in sorted(sects, reverse=(not leftToRight)):
         if not isinstance(p, point.Point):
             continue
         
         t = self.parametricValueFromPoint(p)
         p2 = self.pointFromParametricValue(t - 0.001)
         p3 = self.pointFromParametricValue(t + 0.001)
         
         if CE(p2.y, p3.y):
             continue
         
         if p2.y > p3.y:
             r[leftToRight] += 1
         else:
             r[not leftToRight] += 1
     
     return r
Пример #5
0
    def pointFromParametricValue(self, t):
        """
        Given a parametric value t, where t=0 maps to self.onCurve1 and t=1
        maps to self.onCurve2, returns a Point representing that value.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b.pointFromParametricValue(0))
        (2, -3)
        >>> print(b.pointFromParametricValue(1))
        (4, 4)
        >>> print(b.pointFromParametricValue(0.5))
        (1.5, 0.75)
        """

        if self.offCurve is None:
            # linear case
            x = (t * self.onCurve2.x) + ((1.0 - t) * self.onCurve1.x)
            y = (t * self.onCurve2.y) + ((1.0 - t) * self.onCurve1.y)

        else:
            # quadratic case
            factor0 = (1 - t)**2
            factor1 = 2 * t * (1 - t)
            factor2 = t * t

            x = ((factor0 * self.onCurve1.x) + (factor1 * self.offCurve.x) +
                 (factor2 * self.onCurve2.x))

            y = ((factor0 * self.onCurve1.y) + (factor1 * self.offCurve.y) +
                 (factor2 * self.onCurve2.y))

        return point.Point(x, y)
Пример #6
0
    def piece(self, t1, t2):
        """
        Create a new b-spline mapping the original parametric values in the
        range t1 to t2 into new values from 0 to 1.

        Returns two things: the new b-spline, and an anonymous function which
        maps an old t-value (i.e. a value in the range from t1 through t2) onto
        a new u-value from 0 through 1.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> bNew, f = b.piece(0.25, 0.75)
        >>> print(bNew)
        Curve from (1.375, -1.0625) to (2.375, 2.4375) with off-curve point (1.125, 0.8125)
        >>> for t in (0.0, 0.25, 0.5, 0.75, 1.0):
        ...   tNew = f(t)
        ...   print(b.pointFromParametricValue(t), bNew.pointFromParametricValue(tNew))
        (2.0, -3.0) (2.0, -3.0)
        (1.375, -1.0625) (1.375, -1.0625)
        (1.5, 0.75) (1.5, 0.75)
        (2.375, 2.4375) (2.375, 2.4375)
        (4.0, 4.0) (4.0, 4.0)
        >>> c = BSpline(P(2, -3), P(4, 4), None)
        >>> c.piece(0,10)[0].pprint()
        onCurve1:
          0: 2.0
          1: -3.0
        onCurve2:
          0: 22.0
          1: 67.0
        offCurve: (no data)
        
        >>> c.piece(100,-10)[0].pprint()
        onCurve1:
          0: -18.0
          1: -73.0
        onCurve2:
          0: 202.0
          1: 697.0
        offCurve: (no data)
        """

        if t1 > t2:
            t1, t2 = t2, t1

        newP0 = self.pointFromParametricValue(t1)
        newP2 = self.pointFromParametricValue(t2)

        if self.offCurve is None:
            # linear case
            newSpline = BSpline(newP0, newP2)

        else:
            # quadratic case
            newP1 = self.pointFromParametricValue((t1 + t2) / 2)
            newP1x = 2 * (newP1.x - 0.25 * (newP0.x + newP2.x))
            newP1y = 2 * (newP1.y - 0.25 * (newP0.y + newP2.y))
            newSpline = BSpline(newP0, newP2, point.Point(newP1x, newP1y))

        return newSpline, lambda x: ((x - t1) / (t2 - t1))
Пример #7
0
 def pointFromParametricValue(self, t):
     """
     Returns a Point representing the specified parametric value.
     
     >>> obj = _testingValues[0]
     >>> print(obj.pointFromParametricValue(0))
     (1, 2)
     
     >>> print(obj.pointFromParametricValue(1))
     (10, 7)
     
     >>> print(obj.pointFromParametricValue(0.5))
     (4.0, 5.25)
     
     >>> print(obj.pointFromParametricValue(0.25))
     (2.125, 4.1875)
     
     >>> print(obj.pointFromParametricValue(1.5))
     (19.0, 13.25)
     
     >>> print(obj.pointFromParametricValue(-0.75))
     (2.125, -18.8125)
     """
     
     if self.isLine():
         L = line.Line(self.start, self.end)
         return L.pointFromParametricValue(t)
     
     if t == 0:
         return self.start
     
     if t == 1:
         return self.end
     
     a = self.start.x
     b = self.control0.x
     c = self.control1.x
     d = self.end.x
     e = self.start.y
     f = self.control0.y
     g = self.control1.y
     h = self.end.y
     
     x = (
       (-a + 3 * b - 3 * c + d) * (t ** 3) +
       (3 * a - 6 * b + 3 * c) * (t ** 2) +
       (-3 * a + 3 * b) * t +
       a)
     
     y = (
       (-e + 3 * f - 3 * g + h) * (t ** 3) +
       (3 * e - 6 * f + 3 * g) * (t ** 2) +
       (-3 * e + 3 * f) * t +
       e)
     
     return point.Point(x, y)
Пример #8
0
    def center(self):
        """
        Returns a Point representing the center of the rectangle.
        
        >>> print(_testingValues[1].center())
        (50.0, 40.0)
        """

        return point.Point((self.xMin + self.xMax) / 2,
                           (self.yMin + self.yMax) / 2)
Пример #9
0
    def parametricValueFromProjectedPoint(self, p):
        """
        Given a Point, not necessarily on the line, find the t-value
        representing the projection of that point onto the line. The returned
        t-value is not necessarily in the [0, 1] range.
        
        If self is degenerate, zero is returned.
        
        >>> P = point.Point
        >>> Line(P(0, 0), P(10, 0)).parametricValueFromProjectedPoint(P(2, 6))
        0.2
        >>> Line(P(0, 0), P(0, 10)).parametricValueFromProjectedPoint(P(2, 6))
        0.6
        >>> Line(P(1, 1), P(5, 4)).parametricValueFromProjectedPoint(P(2, 4))
        0.52
        >>> Line(P(1, 1), P(5, 4)).parametricValueFromProjectedPoint(P(25, 4))
        4.2
        
        Degenerate lines always return 0, regardless of the point:
        
        >>> Line(P(2, -3), P(2, -3)).parametricValueFromProjectedPoint(P(0, 0))
        0.0
        """

        m = self.slope()

        if m is None:
            return 0.0

        if abs(m) == float("+inf"):
            # line is vertical
            return self.parametricValueFromPoint(point.Point(self.p1.x, p.y))

        if mathutilities.zeroEnough(m):
            # line is horizontal
            return self.parametricValueFromPoint(point.Point(p.x, self.p1.y))

        # line is neither vertical nor horizontal
        numer = self.p1.y - p.y - (m * self.p1.x) - ((1 / m) * p.x)
        denom = -m - (1 / m)
        x = numer / denom
        return (x - self.p1.x) / (self.p2.x - self.p1.x)
Пример #10
0
 def piece(self, t1, t2):
     """
     Create and return a new CSpline object which maps t1 to t2 in the
     original to 0 to 1 in the new.
     
     Returns two things: the new CSpline, and an anonymous function which
     maps an old t-value to a new u-value.
     
     >>> origObj = _testingValues[0]
     >>> origObj.pprint()
     Start point:
       0: 1
       1: 2
     First control point:
       0: 2
       1: 6
     Second control point:
       0: 5
       1: 5
     End point:
       0: 10
       1: 7
     >>> newObj, f = origObj.piece(0.25, 0.75)
     >>> newObj.pprint()
     Start point:
       0: 2.125
       1: 4.1875
     First control point:
       0: 3.125
       1: 5.1875
     Second control point:
       0: 4.625
       1: 5.4375
     End point:
       0: 6.625
       1: 5.9375
     >>> print(origObj.pointFromParametricValue(0.25))
     (2.125, 4.1875)
     >>> print(newObj.pointFromParametricValue(0))
     (2.125, 4.1875)
     >>> print(origObj.pointFromParametricValue(0.75))
     (6.625, 5.9375)
     >>> print(newObj.pointFromParametricValue(1))
     (6.625, 5.9375)
     """
     
     if self.isLine():
         return line.Line(self.start, self.end).piece(t1, t2)
     
     A, B, C, D, E, F, G, H = self._coefficients()
     
     if t1 > t2:
         t1, t2 = t2, t1
     
     a = t2 - t1
     b = t1
     A2 = A * (a ** 3)
     B2 = 3 * A * a * a * b + B * a * a
     C2 = 3 * A * a * b * b + 2 * B * a * b + C * a
     D2 = A * (b ** 3) + B * b * b + C * b + D
     E2 = E * (a ** 3)
     F2 = 3 * E * a * a * b + F * a * a
     G2 = 3 * E * a * b * b + 2 * F * a * b + G * a
     H2 = E * (b ** 3) + F * b * b + G * b + H
     pVec = self._parametersToPoints(A2, B2, C2, D2, E2, F2, G2, H2)
     
     newSpline = type(self)(
       point.Point(pVec[0], pVec[4]),
       point.Point(pVec[1], pVec[5]),
       point.Point(pVec[2], pVec[6]),
       point.Point(pVec[3], pVec[7]))
     
     return newSpline, lambda x: ((x - t1) / (t2 - t1))
Пример #11
0
class CSpline(object, metaclass=simplemeta.FontDataMetaclass):
    """
    Objects representing cubic b-splines. These are simple collections of 4
    attributes, representing the start and end points and the two control
    points.
    """
    
    #
    # Class definition variables
    #
    
    attrSpec = dict(
        start = dict(
            attr_followsprotocol = True,
            attr_initfunc = (lambda: point.Point(0, 0)),
            attr_label = "Start point"),
        
        control0 = dict(
            attr_followsprotocol = True,
            attr_initfunc = (lambda: point.Point(0, 0)),
            attr_label = "First control point"),
        
        control1 = dict(
            attr_followsprotocol = True,
            attr_initfunc = (lambda: point.Point(0, 0)),
            attr_label = "Second control point"),
        
        end = dict(
            attr_followsprotocol = True,
            attr_initfunc = (lambda: point.Point(0, 0)),
            attr_label = "End point"))
    
    attrSorted = ('start', 'control0', 'control1', 'end')
    
    #
    # Methods
    #
    
    def __str__(self):
        """
        Returns a nice string representation of the CSpline object.
        
        >>> for obj in _testingValues: print(obj)
        Curve from (1, 2) to (10, 7) with controls points (2, 6) and (5, 5)
        Curve from (1, 1) to (19, 1) with controls points (17, 9) and (3, 8)
        Line from (1, 1) to (5, 5)
        Curve from (4, 5.25) to (19, 13.25) with controls points (7, 6.25) and (12, 6.25)
        Curve from (1, 1) to (4, 1) with controls points (2, 10) and (3, -10)
        Curve from (1, 1) to (7, 12) with coincident control points at (4, 3)
        Point at (4, -3)
        """
        
        if self.start.equalEnough(self.end):
            return "Point at %s" % (self.start,)
        
        if self.isLine():
            return "Line from %s to %s" % (self.start, self.end)
        
        if self.control0.equalEnough(self.control1):
            return (
              "Curve from %s to %s with coincident control points at %s" %
              (self.start, self.end, self.control0))
        
        return (
          "Curve from %s to %s with controls points %s and %s" %
          (self.start, self.end, self.control0, self.control1))
    
    def _coefficients(self):
        """
        Returns a tuple with 8 elements, being the numeric factors in the
        expansion of self into the form:
        
            x(t) = At^3 + Bt^2 + Ct + D
            y(t) = Et^3 + Ft^2 + Gt + H
        
        That is, the tuple (A, B, C, D, E, F, G, H) is returned.
        
        >>> print(_testingValues[0]._coefficients())
        (0, 6, 3, 1, 8, -15, 12, 2)
        
        >>> print(_testingValues[1]._coefficients())
        (60, -90, 48, 1, 3, -27, 24, 1)
        
        >>> print(_testingValues[3]._coefficients())
        (0, 6, 9, 4, 8.0, -3.0, 3.0, 5.25)
        """
        
        return self._pointsToParameters(
          self.start.x,
          self.control0.x,
          self.control1.x,
          self.end.x,
          self.start.y,
          self.control0.y,
          self.control1.y,
          self.end.y)
    
    @staticmethod
    def _parametersToPoints(A, B, C, D, E, F, G, H):
        a = D
        b = (C + 3 * a) / 3
        c = (B + 6 * b - 3 * a) / 3
        d = A + a - 3 * b + 3 * c
        e = H
        f = (G + 3 * e) / 3
        g = (F + 6 * f - 3 * e) / 3
        h = E + e - 3 * f + 3 * g
        
        return (a, b, c, d, e, f, g, h)
    
    @staticmethod
    def _pointsToParameters(a, b, c, d, e, f, g, h):
        A = 3 * b - 3 * c + d - a
        B = 3 * a + 3 * c - 6 * b
        C = 3 * b - 3 * a
        D = a
        E = 3 * f - 3 * g + h - e
        F = 3 * e + 3 * g - 6 * f
        G = 3 * f - 3 * e
        H = e
        
        return (A, B, C, D, E, F, G, H)
    
    def area(self):
        """
        Analytical solution for the area bounded by the curve and the line
        connecting self.start and self.end. Note this is sign-sensitive, so the
        area might not be the value you might expect.
        
        >>> round(_testingValues[0].area(), 10)
        8.7
        >>> _testingValues[4].area()
        -1.5
        >>> _testingValues[2].area()
        12.0
        """
        
        width = self.end.x - self.start.x
        trapezoidArea = width * (self.start.y + self.end.y) / 2
        
        if self.isLine():
            return trapezoidArea
        
        A, B, C, D, E, F, G, H = self._coefficients()
        tY = (H, G, F, E)
        tX = (D, C, B, A)
        txDeriv = mathutilities.polyderiv(tX)
        tProd = mathutilities.polymul(tY, txDeriv)
        return sum(mathutilities.polyinteg(tProd)) - trapezoidArea
    
    def bounds(self):
        """
        Returns a Rectangle representing the actual bounds, based on the curve
        itself.
        
        >>> print(_testingValues[0].bounds())
        Minimum X = 1, Minimum Y = 2, Maximum X = 10, Maximum Y = 7
        
        >>> print(_testingValues[1].bounds())
        Minimum X = 1, Minimum Y = 1, Maximum X = 19, Maximum Y = 6.631236180096162
        
        >>> print(_testingValues[2].bounds())
        Minimum X = 1, Minimum Y = 1, Maximum X = 5, Maximum Y = 5
        """
        
        if self.isLine():
            return line.Line(self.start, self.end).bounds()
        
        extremaRect = self.extrema(True)
        a = self.start.x
        b = self.control0.x
        c = self.control1.x
        d = self.end.x
        e = self.start.y
        f = self.control0.y
        g = self.control1.y
        h = self.end.y
        ZE = mathutilities.zeroEnough
        CES = mathutilities.closeEnoughSpan
        
        # xZeroes are where dx/dt goes to zero (i.e. vertical tangents)
        xZeroes = mathutilities.quadratic(
          3 * (-a + 3 * b - 3 * c + d),
          2 * (3 * a - 6 * b + 3 * c),
          (-3 * a + 3 * b))
        
        xZeroes = [n for n in xZeroes if ZE(n.imag) if CES(n)]
        
        # yZeroes are where dy/dt goes to zero (i.e. horizontal tangents)
        yZeroes = mathutilities.quadratic(
          3 * (-e + 3 * f - 3 * g + h),
          2 * (3 * e - 6 * f + 3 * g),
          (-3 * e + 3 * f))
        
        yZeroes = [n for n in yZeroes if ZE(n.imag) if CES(n)]
        
        pX = [
          self.pointFromParametricValue(root)
          for root in xZeroes]
        
        pY = [
          self.pointFromParametricValue(root)
          for root in yZeroes
          if root not in xZeroes]
        
        vXMin = [extremaRect.xMin] + [p.x for p in pX]
        vXMax = [extremaRect.xMax] + [p.x for p in pX]
        vYMin = [extremaRect.yMin] + [p.y for p in pY]
        vYMax = [extremaRect.yMax] + [p.y for p in pY]
        
        return rectangle.Rectangle(
          min(vXMin),
          min(vYMin),
          max(vXMax),
          max(vYMax))
    
    def distanceToPoint(self, p):
        """
        Returns an unsigned distance from the point to the nearest position on
        the curve. This will perforce be a point normal to the curve, but as
        there may be several such, the distance returned will be the smallest.
        
        >>> P = point.Point
        >>> print(_testingValues[0].distanceToPoint(P(5, 3)))
        2.4471964319857658
        
        >>> print(_testingValues[1].distanceToPoint(P(10, 10)))
        3.3691151093862604
        
        >>> print(_testingValues[2].distanceToPoint(P(6,6)))
        0.0
        """
        
        if self.isLine():
            L = line.Line(self.start, self.end)
            return L.distanceToPoint(p)
        
        a = self.start.x
        b = self.control0.x
        c = self.control1.x
        d = self.end.x
        e = self.start.y
        f = self.control0.y
        g = self.control1.y
        h = self.end.y
        A = 3 * b - 3 * c + d - a
        B = 3 * a + 3 * c - 6 * b
        C = 3 * b - 3 * a
        D = 3 * f - 3 * g + h - e
        E = 3 * e + 3 * g - 6 * f
        F = 3 * f - 3 * e
        G = a - p.x
        H = e - p.y
        
        def sumPrime(t):
            return (
              6 * (A * A + D * D) * (t ** 5) +
              10 * (A * B + D * E) * (t ** 4) +
              4 * (B * B + E * E + 2 * A * C + 2 * D * F) * (t ** 3) +
              6 * (A * G + B * C + D * H + E * F) * (t * t) +
              2 * (C * C + F * F + 2 * B * G + 2 * E * H) * t +
              2 * (C * G + F * H))
        
        def sumDoublePrime(t):
            return (
              30 * (A * A + D * D) * (t ** 4) +
              40 * (A * B + D * E) * (t ** 3) +
              12 * (B * B + E * E + 2 * A * C + 2 * D * F) * (t * t) +
              12 * (A * G + B * C + D * H + E * F) * t +
              2 * (C * C + F * F + 2 * B * G + 2 * E * H))
        
        tReal = mathutilities.newton(sumPrime, sumDoublePrime, 0.5)
        
        # Now we remove this root from the quintic, leaving a quartic that can
        # be solved directly.
        
        factors = (
          2 * (C * G + F * H),
          2 * (C * C + F * F + 2 * B * G + 2 * E * H),
          6 * (A * G + B * C + D * H + E * F),
          4 * (B * B + E * E + 2 * A * C + 2 * D * F),
          10 * (A * B + D * E),
          6 * (A * A + D * D))
        
        reducedFactors = mathutilities.polyreduce(factors, tReal)
        roots = (tReal,) + mathutilities.quartic(*reversed(reducedFactors))
        ZE = mathutilities.zeroEnough
        
        v = [
          p.distanceFrom(self.pointFromParametricValue(root))
          for root in roots
          if ZE(root.imag)]
        
        return utilities.safeMin(v)
    
    def extrema(self, excludeOffCurve=False):
        """
        Returns a Rectangle representing the extrema (based on the actual point
        coordinates, and not the curve itself).
        
        >>> print(_testingValues[0].extrema())
        Minimum X = 1, Minimum Y = 2, Maximum X = 10, Maximum Y = 7
        
        >>> print(_testingValues[2].extrema())
        Minimum X = 1, Minimum Y = 1, Maximum X = 5, Maximum Y = 5
        """
        
        if self.isLine():
            return line.Line(self.start, self.end).extrema()
        
        if excludeOffCurve:
            ps = [self.start, self.end]
        else:
            ps = [self.start, self.control0, self.control1, self.end]
        
        return rectangle.Rectangle(
          min(p.x for p in ps),
          min(p.y for p in ps),
          max(p.x for p in ps),
          max(p.y for p in ps))
    
    def intersection(self, other, delta=1.0e-5):
        """
        Returns a tuple of objects representing the intersection of the two
        CSpline objects, usually Points but possibly a CSpline; empty if there
        is no intersection.
        
        We use Newton's method here to solve the nonlinear system:
        
            self_x(t) - other_x(u) = 0
            self_y(t) - other_y(u) = 0
        
        There is an admittedly ad hoc decision here to look at 25 different
        (t, u) pairs as starting values to find local solutions. This method
        has (so far) found all solutions for intersections I've thrown at it,
        including one case with 5 intersections, but this heuristic code should
        be rigorously tested.
        
        Also, the resulting t- and u-values are rounded to six places.
        
        >>> for obj in _testingValues[0].intersection(_testingValues[3]):
        ...     obj.pprint()
        Start point:
          0: 4.0
          1: 5.25
        First control point:
          0: 5.5
          1: 5.75
        Second control point:
          0: 7.5
          1: 6.0
        End point:
          0: 10.0
          1: 7.0
          
        >>> P = point.Point
        >>> a = CSpline(P(0, 0), P(3, 3), P(4, 4), P(6, 6))
        >>> for obj in _testingValues[2].intersection(a):
        ...   print(str(obj))
        Line from (1, 1) to (5, 5)

        >>> for obj in _testingValues[2].intersection(_testingValues[2]):
        ...   print(str(obj))
        Line from (1, 1) to (5, 5)
        
        >>> c = CSpline(P(0, 0), P(-1, 0), P(0, 0), P(0, 0))
        >>> print(_testingValues[2].intersection(c))
        ()
        """
        
        # Before we start working on potential point intersections, do a check
        # to see if the entire cubics overlap, or if they're both lines.
        
        selfLine = self.isLine()
        otherLine = other.isLine()
        
        if selfLine or otherLine:
            if selfLine and otherLine:
                L1 = line.Line(self.start, self.end)
                L2 = line.Line(other.start, other.end)
                sect = L1.intersection(L2, delta)
                
                if sect is None:
                    return ()
                
                return (sect,)
            
            if selfLine and (self.control0 is None and self.control1 is None):
                L1 = line.Line(self.start, self.end)
                
                self = type(self)(
                  self.start,
                  L1.midpoint(),
                  L1.midpoint(),
                  self.end)
            
            if otherLine and (other.control0 is None and other.control1 is None):
                L1 = line.Line(other.start, other.end)
                
                other = type(other)(
                  other.start,
                  L1.midpoint(),
                  L1.midpoint(),
                  other.end)
        
        pvsSelf = [0.0, 0.25, 0.5, 0.75, 1.0]
        pts = [self.pointFromParametricValue(n) for n in pvsSelf]
        pvsOther = [other.parametricValueFromPoint(pt) for pt in pts]
        ZE = mathutilities.zeroEnough
        
        if None not in pvsOther:
            diffs = [i - j for i, j in zip(pvsSelf, pvsOther)]
            
            if ZE(max(diffs) - min(diffs)):
                d = diffs[0]
                
                if not (ZE(d) or ZE(d - 1) or (0 < d < 1)):
                    return ()
                
                elif ZE(d):
                    return (self.__deepcopy__(),)
                
                elif d < 0:
                    return (self.piece(0, 1 + d)[0],)
                
                return (self.piece(d, 1)[0],)
        
        # If we get here, there is no overlap, so proceed with the actual
        # calculations.
        
        A, B, C, D, E, F, G, H = self._coefficients()
        S, T, U, V, W, X, Y, Z = other._coefficients()
        
        def func(v):  # maps a (u, v) pair to a distance in x and y
            t, u = v
            
            x = (
              A * (t ** 3)
              + B * t * t
              + C * t
              + D
              - S * (u ** 3)
              - T * u * u
              - U * u
              - V)
            
            y = (
              E * (t ** 3)
              + F * t * t
              + G * t
              + H
              - W * (u ** 3)
              - X * u * u
              - Y * u
              - Z)
    
            return [x, y]

        def Jacobian(t, u):
            e1 = 3 * A * t * t + 2 * B * t + C
            e2 = -3 * S * u * u - 2 * T * u - U
            e3 = 3 * E * t * t + 2 * F * t + G
            e4 = -3 * W * u * u - 2 * X * u - Y
            return [[e1, e2], [e3, e4]]
        
        def matInv(m):
            det = m[0][0] * m[1][1] - m[0][1] * m[1][0]
            
            return [
              [m[1][1] / det, -m[0][1] / det],
              [-m[1][0] / det, m[0][0] / det]]
        
        def matMul(m, v):
            e1 = m[0][0] * v[0] + m[0][1] * v[1]
            e2 = m[1][0] * v[0] + m[1][1] * v[1]
            return [e1, e2]
        
        solutions = set()
        zeroDivCount = 0
        giveUpCount = 0
        
        for n1 in range(5):
            for n2 in range(5):
                n = [n1 / 4, n2 / 4]
                nPrior = [-1, -1]
                iterCount = 0
                okToProceed = True
                
                while (not ZE(n[0] - nPrior[0])) or (not ZE(n[1] - nPrior[1])):
                    try:
                        nDelta = matMul(matInv(Jacobian(*n)), func(n))
                    
                    except ZeroDivisionError:
                        zeroDivCount += 1
                        okToProceed = False
                    
                    if not okToProceed:
                        break
                    
                    nPrior = list(n)
                    n[0] -= nDelta[0]
                    n[1] -= nDelta[1]
                    iterCount += 1
                
                    if iterCount > 10:
                        giveUpCount += 1
                        okToProceed = False
                        break
                
                if okToProceed:
                    if not (ZE(n[0]) or ZE(n[0] - 1) or (0 < n[0] < 1)):
                        continue
                
                    if not (ZE(n[1]) or ZE(n[1] - 1) or (0 < n[1] < 1)):
                        continue
                
                    checkIt = func(n)
                
                    if (not ZE(checkIt[0])) or (not ZE(checkIt[1])):
                        continue
                
                    n = (round(n[0], 6), round(n[1], 6))
                    solutions.add(n)
        
        return tuple(
          self.pointFromParametricValue(obj[0])
          for obj in solutions)
    
    def isLine(self):
        """
        Returns True if the two control points are colinear with the start and
        end points (or are both None), False otherwise.
        
        >>> _testingValues[1].isLine()
        False
        
        >>> _testingValues[2].isLine()
        True
        """
        
        if self.control0 is None and self.control1 is None:
            return True
        
        L = line.Line(self.start, self.end)
        
        if L.parametricValueFromPoint(self.control0) is None:
            return False
        
        return L.parametricValueFromPoint(self.control1) is not None
    
    def magnified(self, xScale=1, yScale=1):
        """
        Returns a new CSpline scaled as specified about the origin.
        """
        
        if self.isLine():
            return type(self)(
              self.start.magnified(xScale, yScale),
              None,
              None,
              self.end.magnified(xScale, yScale))
        
        return type(self)(
          self.start.magnified(xScale, yScale),
          self.control0.magnified(xScale, yScale),
          self.control1.magnified(xScale, yScale),
          self.end.magnified(xScale, yScale))
    
    def magnifiedAbout(self, about, xScale=1, yScale=1):
        """
        Returns a new CSpline scaled as specified about the specified point.
        """
        
        if self.isLine():
            return type(self)(
              self.start.magnifiedAbout(about, xScale, yScale),
              None,
              None,
              self.end.magnifiedAbout(about, xScale, yScale))
        
        return type(self)(
          self.start.magnifiedAbout(about, xScale, yScale),
          self.control0.magnifiedAbout(about, xScale, yScale),
          self.control1.magnifiedAbout(about, xScale, yScale),
          self.end.magnifiedAbout(about, xScale, yScale))
    
    def moved(self, deltaX=0, deltaY=0):
        """
        Returns a new CSpline moved by the specified amount.
        """
        
        if self.isLine():
            return type(self)(
              self.start.moved(deltaX, deltaY),
              None,
              None,
              self.end.moved(deltaX, deltaY))
        
        return type(self)(
          self.start.moved(deltaX, deltaY),
          self.control0.moved(deltaX, deltaY),
          self.control1.moved(deltaX, deltaY),
          self.end.moved(deltaX, deltaY))
    
    def parametricValueFromPoint(self, p):
        """
        Returns the parametric t-value for the specified point p, or None if p
        does not lie on the spline.
        
        >>> obj = _testingValues[0]
        >>> print(obj.parametricValueFromPoint(point.Point(1, 2)))
        0
        
        >>> print(obj.parametricValueFromPoint(point.Point(10, 7)))
        1
        
        >>> print(obj.parametricValueFromPoint(point.Point(4, 5.25)))
        0.5
        
        >>> print(obj.parametricValueFromPoint(point.Point(4, -5.25)))
        None
        """
        
        if self.isLine():
            L = line.Line(self.start, self.end)
            return L.parametricValueFromPoint(p)
        
        a = self.start.x
        b = self.control0.x
        c = self.control1.x
        d = self.end.x
        
        roots = mathutilities.cubic(
          (-a + 3 * b - 3 * c + d),
          (3 * a - 6 * b + 3 * c),
          (-3 * a + 3 * b),
          (a - p.x))
        
        # Check that the y-value for real roots is sufficiently close
        
        for root in roots:
            if not mathutilities.zeroEnough(root.imag):
                continue
            
            pTest = self.pointFromParametricValue(root)
            
            if mathutilities.closeEnough(p.y, pTest.y):
                return root
        
        return None
    
    def piece(self, t1, t2):
        """
        Create and return a new CSpline object which maps t1 to t2 in the
        original to 0 to 1 in the new.
        
        Returns two things: the new CSpline, and an anonymous function which
        maps an old t-value to a new u-value.
        
        >>> origObj = _testingValues[0]
        >>> origObj.pprint()
        Start point:
          0: 1
          1: 2
        First control point:
          0: 2
          1: 6
        Second control point:
          0: 5
          1: 5
        End point:
          0: 10
          1: 7
        >>> newObj, f = origObj.piece(0.25, 0.75)
        >>> newObj.pprint()
        Start point:
          0: 2.125
          1: 4.1875
        First control point:
          0: 3.125
          1: 5.1875
        Second control point:
          0: 4.625
          1: 5.4375
        End point:
          0: 6.625
          1: 5.9375
        >>> print(origObj.pointFromParametricValue(0.25))
        (2.125, 4.1875)
        >>> print(newObj.pointFromParametricValue(0))
        (2.125, 4.1875)
        >>> print(origObj.pointFromParametricValue(0.75))
        (6.625, 5.9375)
        >>> print(newObj.pointFromParametricValue(1))
        (6.625, 5.9375)
        """
        
        if self.isLine():
            return line.Line(self.start, self.end).piece(t1, t2)
        
        A, B, C, D, E, F, G, H = self._coefficients()
        
        if t1 > t2:
            t1, t2 = t2, t1
        
        a = t2 - t1
        b = t1
        A2 = A * (a ** 3)
        B2 = 3 * A * a * a * b + B * a * a
        C2 = 3 * A * a * b * b + 2 * B * a * b + C * a
        D2 = A * (b ** 3) + B * b * b + C * b + D
        E2 = E * (a ** 3)
        F2 = 3 * E * a * a * b + F * a * a
        G2 = 3 * E * a * b * b + 2 * F * a * b + G * a
        H2 = E * (b ** 3) + F * b * b + G * b + H
        pVec = self._parametersToPoints(A2, B2, C2, D2, E2, F2, G2, H2)
        
        newSpline = type(self)(
          point.Point(pVec[0], pVec[4]),
          point.Point(pVec[1], pVec[5]),
          point.Point(pVec[2], pVec[6]),
          point.Point(pVec[3], pVec[7]))
        
        return newSpline, lambda x: ((x - t1) / (t2 - t1))
    
    def pointFromParametricValue(self, t):
        """
        Returns a Point representing the specified parametric value.
        
        >>> obj = _testingValues[0]
        >>> print(obj.pointFromParametricValue(0))
        (1, 2)
        
        >>> print(obj.pointFromParametricValue(1))
        (10, 7)
        
        >>> print(obj.pointFromParametricValue(0.5))
        (4.0, 5.25)
        
        >>> print(obj.pointFromParametricValue(0.25))
        (2.125, 4.1875)
        
        >>> print(obj.pointFromParametricValue(1.5))
        (19.0, 13.25)
        
        >>> print(obj.pointFromParametricValue(-0.75))
        (2.125, -18.8125)
        """
        
        if self.isLine():
            L = line.Line(self.start, self.end)
            return L.pointFromParametricValue(t)
        
        if t == 0:
            return self.start
        
        if t == 1:
            return self.end
        
        a = self.start.x
        b = self.control0.x
        c = self.control1.x
        d = self.end.x
        e = self.start.y
        f = self.control0.y
        g = self.control1.y
        h = self.end.y
        
        x = (
          (-a + 3 * b - 3 * c + d) * (t ** 3) +
          (3 * a - 6 * b + 3 * c) * (t ** 2) +
          (-3 * a + 3 * b) * t +
          a)
        
        y = (
          (-e + 3 * f - 3 * g + h) * (t ** 3) +
          (3 * e - 6 * f + 3 * g) * (t ** 2) +
          (-3 * e + 3 * f) * t +
          e)
        
        return point.Point(x, y)
    
    def rotated(self, angleInDegrees=0):
        """
        Returns a new CSpline rotated as specified about the origin.
        """
        
        if self.isLine():
            return type(self)(
              self.start.rotated(angleInDegrees),
              None,
              None,
              self.end.rotated(angleInDegrees))
        
        return type(self)(
          self.start.rotated(angleInDegrees),
          self.control0.rotated(angleInDegrees),
          self.control1.rotated(angleInDegrees),
          self.end.rotated(angleInDegrees))
    
    def rotatedAbout(self, about, angleInDegrees=0):
        """
        Returns a new CSpline rotated as specified about the specified point.
        """
        
        if self.isLine():
            return type(self)(
              self.start.rotatedAbout(about, angleInDegrees),
              None,
              None,
              self.end.rotatedAbout(about, angleInDegrees))
        
        return type(self)(
          self.start.rotatedAbout(about, angleInDegrees),
          self.control0.rotatedAbout(about, angleInDegrees),
          self.control1.rotatedAbout(about, angleInDegrees),
          self.end.rotatedAbout(about, angleInDegrees))
    
    def slopeFromParametricValue(self, t):
        """
        Returns the slope of the curve at the parametric value t.
        
        >>> obj = _testingValues[0]
        >>> print(obj.slopeFromParametricValue(0))
        4.0
        >>> print(obj.slopeFromParametricValue(1))
        0.4
        """
        
        ZE = mathutilities.zeroEnough
        
        if self.isLine():
            # The slope is constant in this case
            p1, p2 = self.start, self.end
            
            if ZE(p2.x - p1.x):
                return float("+inf")
            
            return (p2.y - p1.y) / (p2.x - p1.x)
        
        # Let a, b, c, d be the x-coordinates, in order.
        # Then x = a(1-t)^3 + 3bt(1-t)^2 + 3ct^2(1-t) + dt^3
        # So x = 
        #   (-a + 3b - 3c + d)t^3 +
        #   (3a - 6b + 3c)t^2 +
        #   (-3a + 3b)t +
        #   a
        #
        # Similarly, for e, f, g, h as the y-analogs, we get:
        # y =
        #   (-e + 3f - 3g + h)t^3 +
        #   (3e - 6f + 3g)t^2 +
        #   (-3e + 3f)t +
        #   e
        #
        # The slope is then the ratio of (dy / dt) / (dx / dt).
        
        a = self.start.x
        b = self.control0.x
        c = self.control1.x
        d = self.end.x
        
        denom = (
          3 * (-a + 3 * b - 3 * c + d) * (t ** 2) +
          2 * (3 * a - 6 * b + 3 * c) * t +
          (-3 * a + 3 * b))
        
        if ZE(denom):
            return float("+inf")
        
        e = self.start.y
        f = self.control0.y
        g = self.control1.y
        h = self.end.y
        
        numer = (
          3 * (-e + 3 * f - 3 * g + h) * (t ** 2) +
          2 * (3 * e - 6 * f + 3 * g) * t +
          (-3 * e + 3 * f))
        
        return numer / denom
    
    def transitions(self, xStart, xStop, yCoord, leftToRight):
        """
        Finds the intersection of self and the horizontal line segment defined
        by xStart, xStop, and yCoord, then for each point in the intersection
        determines whether the curve passes by left-to-right or right-to-left
        when looked at in the specified leftToRight orientation. See the
        doctest examples below for a sense of how this works; basically, a dict
        is returned with True mapping to seen left-to-right transitions, and
        False mapping to seen right-to-left transitions.
        
        >>> obj = _testingValues[0]
        >>> print(obj.transitions(0, 10, 3, True))
        {False: 1, True: 0}
        >>> print(obj.transitions(0, 10, 3, False))
        {False: 0, True: 1}
        
        >>> obj = _testingValues[4]
        >>> print(obj.transitions(0, 5, 2, True))
        {False: 1, True: 1}
        >>> print(obj.transitions(0, 5, 1, True))
        {False: 2, True: 1}
        """
        
        ray = type(self)(
          start = point.Point(xStart, yCoord),
          control0 = None,
          control1 = None,
          end = point.Point(xStop, yCoord))
        
        sects = self.intersection(ray)
        r = {False: 0, True: 0}
        CE = mathutilities.closeEnough
        
        for p in sorted(sects, reverse=(not leftToRight)):
            if not isinstance(p, point.Point):
                continue
            
            t = self.parametricValueFromPoint(p)
            p2 = self.pointFromParametricValue(t - 0.001)
            p3 = self.pointFromParametricValue(t + 0.001)
            
            if CE(p2.y, p3.y):
                continue
            
            if p2.y > p3.y:
                r[leftToRight] += 1
            else:
                r[not leftToRight] += 1
        
        return r
Пример #12
0
class Line(object, metaclass=simplemeta.FontDataMetaclass):
    """
    Objects representing finite line segments. These are simple objects with
    two attributes: p1 and p2, both of which are Point objects.
    """

    #
    # Class definition variables
    #

    attrSpec = dict(p1=dict(attr_followsprotocol=True,
                            attr_initfunc=(lambda: point.Point(0, 0))),
                    p2=dict(attr_followsprotocol=True,
                            attr_initfunc=(lambda: point.Point(0, 0))))

    #
    # Methods
    #

    def __str__(self):
        """
        Returns a nicely formatted string representing the Line.
        
        >>> P = point.Point
        >>> print(Line(P(3, 8), P(-1, 15.5)))
        Line from (3, 8) to (-1, 15.5)
        >>> print(Line(P(3, 8), P(3, 8)))
        Degenerate line at (3, 8)
        """

        if self.isDegenerate():
            return "Degenerate line at %s" % (self.p1, )

        return "Line from %s to %s" % (self.p1, self.p2)

    def bounds(self):
        """
        >>> P = point.Point
        >>> L1 = Line(P(1, 1), P(1, 9))
        >>> print(L1.bounds())
        Minimum X = 1, Minimum Y = 1, Maximum X = 1, Maximum Y = 9
        """

        xMin = min(self.p1.x, self.p2.x)
        yMin = min(self.p1.y, self.p2.y)
        xMax = max(self.p1.x, self.p2.x)
        yMax = max(self.p1.y, self.p2.y)
        return rectangle.Rectangle(xMin, yMin, xMax, yMax)

    def distanceToPoint(self, p):
        """
        Returns an unsigned distance from the line to the point. This will be
        the smallest possible distance (i.e. the distance along the normal to
        the line's slope).
        
        >>> P = point.Point
        >>> L1 = Line(P(1, 1), P(1, 9))
        >>> print(L1.distanceToPoint(P(1, 5)))
        0.0
        >>> print(L1.distanceToPoint(P(5, 5)))
        4.0
        >>> print(L1.distanceToPoint(P(-5, 15)))
        6.0
        >>> L3 = Line(P(0,0),P(0,0))
        >>> print(L3.distanceToPoint(P(0, 0)))
        0.0
        >>> L2 = Line(P(3, 1), P(1, -6))
        >>> print(L2.distanceToPoint(P(0, 0)))
        2.609850715025092
        """

        if self.isDegenerate():
            return p.distanceFrom(self.p1)

        t = self.parametricValueFromProjectedPoint(p)
        pOnLine = self.pointFromParametricValue(t)
        return p.distanceFrom(pOnLine)

    def equalEnough(self, other, delta=1.0e-5):
        """
        Returns True if self's two points are within delta of other's two
        points.
        
        >>> P = point.Point
        >>> L1 = Line(P(3, -2), P(4, 4))
        >>> L2 = Line(P(3.001, -1.999), P(4, 4.001))
        >>> L1.equalEnough(L2)
        False
        >>> L1.equalEnough(L2, delta=0.01)
        True
        """

        way1 = (self.p1.equalEnough(other.p1, delta)
                and self.p2.equalEnough(other.p2, delta))

        way2 = (self.p1.equalEnough(other.p2, delta)
                and self.p2.equalEnough(other.p1, delta))

        return way1 or way2

    extrema = bounds  # no difference for lines

    def intersection(self, other, delta=1.0e-5):
        """
        Determines if self and other intersect, and returns an object
        representing that intersection. This could be None if they do not
        intersect, a Point object if they intersect in a single point, or a
        Line object if they are fully or partly coincident.
        
        >>> P = point.Point
        
        Simple point intersection cases:
        
        >>> print(Line(P(0, 0), P(0, 8)).intersection(Line(P(0, 4), P(8, 4))))
        (0.0, 4.0)
        >>> print(Line(P(2, 1), P(5, 4)).intersection(Line(P(3, 3), P(11, 3))))
        (4.0, 3.0)
        
        Non-parallel, out-of-bounds intersection case:
        
        >>> L1 = Line(P(2, 2), P(5, 5))
        >>> L2 = Line(P(20000, 2), P(20004, 5))
        >>> print(L1.intersection(L2))
        None
        
        Parallel line cases, disjoint and otherwise:
        
        >>> print(Line(P(1, 1), P(5, 5)).intersection(Line(P(1, 2), P(5, 6))))
        None
        >>> print(Line(P(1, 1), P(5, 5)).intersection(Line(P(2, 2), P(6, 6))))
        Line from (2.0, 2.0) to (5, 5)
        >>> print(Line(P(0, 0), P(1000000, 1000000)).intersection(Line(P(999999, 999999), P(2000000, 2000000))))
        (999999, 999999)
        >>> print(Line(P(1, 1), P(5, 5)).intersection(Line(P(6, 6), P(8, 8))))
        None
        
        Degenerate line cases:
        >>> print(Line(P(4, 4), P(4, 4)).intersection(Line(P(0, 0), P(5, 5))))
        (4, 4)
        >>> print(Line(P(4, 4), P(4, 4)).intersection(Line(P(1, 0), P(5, 5))))
        None
        >>> print(Line(P(4, 4), P(4, 4)).intersection(Line(P(4, 4), P(4, 4.0000001))))
        (4, 4)
        >>> print(Line(P(4, 4), P(4, 4)).intersection(Line(P(70000, 70000), P(70000, 70000))))
        None
        >>> print(Line(P(1, 1), P(5, 5)).intersection(Line(P(3, 3), P(3, 3))))
        (3, 3)
        >>> print(Line(P(1, 1), P(5, 5)).intersection(Line(P(8, 8), P(8, 8))))
        None
        >>> print(Line(P(1, 1), P(5, 5)).intersection(Line(P(2, 3), P(2, 3))))
        None
        """

        CE = mathutilities.closeEnough
        CES = mathutilities.closeEnoughSpan
        IIP = mathutilities.intIfPossible

        if self.isDegenerate(delta):
            if other.isDegenerate(delta):
                if mathutilities.closeEnoughXY(self.p1, other.p1, delta):
                    return self.p1.__copy__()

                return None

            t = other.parametricValueFromPoint(self.p1)

            if t is None:
                return t

            return self.p1.__copy__()

        elif other.isDegenerate(delta):
            t = self.parametricValueFromPoint(other.p1)

            if (t is None) or (not CES(t)):
                return None

            return other.p1.__copy__()

        if CE(self.slope(), other.slope()):
            # lines are either parallel or coincident
            t1 = self.parametricValueFromPoint(other.p1)

            if t1 is None:
                return None

            # lines are coincident, although the segments may not overlap
            t2 = self.parametricValueFromPoint(other.p2)

            if max(t1, t2) < 0 or min(t1, t2) > 1:
                return None

            t1 = IIP(max(0, t1))
            t2 = IIP(min(1, t2))

            if CE(t1, t2):
                return other.p1

            return type(self)(self.pointFromParametricValue(t1),
                              self.pointFromParametricValue(t2))

        # the slopes differ, so solve the equations
        factorMatrix = matrix.Matrix(
            ((self.p2.x - self.p1.x, other.p1.x - other.p2.x, 0),
             (self.p2.y - self.p1.y, other.p1.y - other.p2.y, 0), (0, 0, 1)))

        fmInverse = factorMatrix.inverse()
        assert fmInverse is not None

        constMatrix = matrix.Matrix(
            ((other.p1.x - self.p1.x, 0, 0), (other.p1.y - self.p1.y, 0, 0),
             (0, 0, 1)))

        prod = fmInverse.multiply(constMatrix)
        t = IIP(prod[0][0])
        u = IIP(prod[1][0])

        if CES(t) and CES(u):
            return self.pointFromParametricValue(t)

        return None  # no intersection within the actual length

    def isDegenerate(self, delta=1.0e-5):
        """
        Returns True if the two points making up the Line are equal enough,
        using the specified delta.
        
        >>> P = point.Point
        >>> Line(P(5, 2), P(5.001, 2)).isDegenerate()
        False
        >>> Line(P(5, 2), P(5.001, 2)).isDegenerate(delta=0.01)
        True
        """

        return self.p1.equalEnough(self.p2, delta=delta)

    def length(self):
        """
        Returns the length of the Line.
        
        >>> P = point.Point
        >>> Line(P(0, 0), P(3, 4)).length()
        5.0
        
        Degenerate lines have zero length:
        
        >>> Line(P(2, -3), P(2, -3)).length()
        0
        """

        if self.isDegenerate():
            return 0

        return math.sqrt(((self.p2.x - self.p1.x)**2) +
                         ((self.p2.y - self.p1.y)**2))

    def magnified(self, xScale=1, yScale=1):
        """
        Returns a new Line scaled as specified about the origin.
        
        >>> P = point.Point
        >>> L = Line(P(3, -2), P(4, 4))
        >>> print(L.magnified(1.75, -0.5))
        Line from (5.25, 1.0) to (7.0, -2.0)
        """

        return type(self)(self.p1.magnified(xScale, yScale),
                          self.p2.magnified(xScale, yScale))

    def magnifiedAbout(self, about, xScale=1, yScale=1):
        """
        Returns a new Line scaled as specified about a specified point.
        
        >>> P = point.Point
        >>> L = Line(P(3, -2), P(4, 4))
        >>> print(L.magnifiedAbout(P(1, -1), xScale=1.75, yScale=-0.5))
        Line from (4.5, -0.5) to (6.25, -3.5)
        """

        return type(self)(self.p1.magnifiedAbout(about, xScale, yScale),
                          self.p2.magnifiedAbout(about, xScale, yScale))

    def midpoint(self):
        """
        Returns a Point representing the midpoint of the Line.
        
        >>> P = point.Point
        >>> print(Line(P(3, -2), P(4, 4)).midpoint())
        (3.5, 1.0)
        """

        return (self.p1 + self.p2) / 2

    def moved(self, deltaX=0, deltaY=0):
        """
        Returns a new Line moved by the specified amount.
        
        >>> P = point.Point
        >>> L = Line(P(3, -2), P(4, 4))
        >>> print(L)
        Line from (3, -2) to (4, 4)
        >>> print(L.moved(-5, 10))
        Line from (-2, 8) to (-1, 14)
        """

        return type(self)(self.p1.moved(deltaX, deltaY),
                          self.p2.moved(deltaX, deltaY))

    def normalized(self, length=1):
        """
        Returns a new Line whose first point is the same as self's, but whose
        second point is moved such that the length of the new Line is as
        specified. The new Line's second point will be in the same direction
        relative to the first point as the original Line's was.
        
        If self is degenerate it cannot be normalized to a non-zero length, so
        a copy of self will be returned if length is zero and None will be
        returned otherwise.
        
        Specifying a length of zero will return a degenerate line both of whose
        points are the same as self.p1.
        
        >>> P = point.Point
        >>> print(Line(P(0, 0), P(10, 0)).normalized())
        Line from (0, 0) to (1.0, 0.0)
        >>> print(Line(P(0, 0), P(0, 10)).normalized())
        Line from (0, 0) to (0, 1)
        >>> print(Line(P(0, 0), P(0, -10)).normalized())
        Line from (0, 0) to (0, -1)
        >>> print(Line(P(1, -1), P(-2, -6)).normalized())
        Line from (1, -1) to (0.48550424457247343, -1.8574929257125443)
        >>> print(Line(P(0, 0), P(10, 0)).normalized(length=5))
        Line from (0, 0) to (5.0, 0.0)
        
        Degenerate cases:
        
        >>> LDeg = Line(P(2, -3), P(2, -3))
        >>> LDeg.normalized() is None
        True
        >>> LDeg.normalized(0) == LDeg
        True
        >>> LDeg.normalized(0) is LDeg
        False
        """

        p = self.p1

        if not length:
            return type(self)(p, p)

        slope = self.slope()

        if slope is None:
            return (None if length else self.__copy__())

        if abs(slope) == float("+inf"):
            # line is vertical
            sign = (-1 if self.p2.y < p.y else 1)
            return type(self)(p, point.Point(p.x, p.y + sign * length))

        sign = (-1 if self.p2.x < p.x else 1)
        x = p.x + sign * math.sqrt(length**2 / (1 + slope**2))
        y = p.y + slope * (x - p.x)
        return type(self)(p, point.Point(x, y))

    def parametricValueFromProjectedPoint(self, p):
        """
        Given a Point, not necessarily on the line, find the t-value
        representing the projection of that point onto the line. The returned
        t-value is not necessarily in the [0, 1] range.
        
        If self is degenerate, zero is returned.
        
        >>> P = point.Point
        >>> Line(P(0, 0), P(10, 0)).parametricValueFromProjectedPoint(P(2, 6))
        0.2
        >>> Line(P(0, 0), P(0, 10)).parametricValueFromProjectedPoint(P(2, 6))
        0.6
        >>> Line(P(1, 1), P(5, 4)).parametricValueFromProjectedPoint(P(2, 4))
        0.52
        >>> Line(P(1, 1), P(5, 4)).parametricValueFromProjectedPoint(P(25, 4))
        4.2
        
        Degenerate lines always return 0, regardless of the point:
        
        >>> Line(P(2, -3), P(2, -3)).parametricValueFromProjectedPoint(P(0, 0))
        0.0
        """

        m = self.slope()

        if m is None:
            return 0.0

        if abs(m) == float("+inf"):
            # line is vertical
            return self.parametricValueFromPoint(point.Point(self.p1.x, p.y))

        if mathutilities.zeroEnough(m):
            # line is horizontal
            return self.parametricValueFromPoint(point.Point(p.x, self.p1.y))

        # line is neither vertical nor horizontal
        numer = self.p1.y - p.y - (m * self.p1.x) - ((1 / m) * p.x)
        denom = -m - (1 / m)
        x = numer / denom
        return (x - self.p1.x) / (self.p2.x - self.p1.x)

    def parametricValueFromPoint(self, p):
        """
        Given a Point, returns the t value that satisfies the parametric
        equation: (1-t)p1 + (t)p2 == p. Returns None if p does not intersect
        self.
        
        >>> P = point.Point
        >>> L = Line(P(1, 1), P(3, 3))
        >>> print(L.parametricValueFromPoint(P(1, 1)))
        0.0
        >>> print(L.parametricValueFromPoint(P(2, 2)))
        0.5
        >>> print(L.parametricValueFromPoint(P(2, 3)))
        None
        
        >>> L = Line(P(1, 1), P(3, 1))  # horizontal
        >>> print(L.parametricValueFromPoint(P(2, 1)))
        0.5
        >>> print(L.parametricValueFromPoint(P(2, 2)))
        None
        
        >>> L = Line(P(1, 1), P(1, 3))  # vertical
        >>> print(L.parametricValueFromPoint(P(1, 2)))
        0.5
        >>> print(L.parametricValueFromPoint(P(2, 2)))
        None
        
        >>> L = Line(P(1, 1), P(1, 1))  # degenerate (point)
        >>> print(L.parametricValueFromPoint(P(1, 1)))
        0.0
        >>> print(L.parametricValueFromPoint(P(2, 2)))
        None
        """

        ce = mathutilities.closeEnough
        p1 = self.p1
        p2 = self.p2

        if ce(p1.x, p2.x):
            tx = None
        else:
            tx = (p.x - p1.x) / (p2.x - p1.x)

        if ce(p1.y, p2.y):
            ty = None
        else:
            ty = (p.y - p1.y) / (p2.y - p1.y)

        if tx is None and ty is None:
            # degenerate case: self is a Point
            return (0.0 if p1.equalEnough(p) else None)

        elif tx is None:
            return (ty if ce(p.x, p1.x) else None)

        elif ty is None:
            return (tx if ce(p.y, p1.y) else None)

        return (tx if ce(tx, ty) else None)

    def perpendicularTo(self, other):
        """
        Returns True if the two Lines are orthogonal, False otherwise.
        
        >>> P = point.Point
        >>> Line(P(0, 0), P(1, 0)).perpendicularTo(Line(P(0, 0), P(0, 1)))
        True
        >>> Line(P(1, 1), P(2, 2)).perpendicularTo(Line(P(-6, 5), P(-7, 6)))
        True
        >>> Line(P(1, 1), P(2, 3)).perpendicularTo(Line(P(-6, 5), P(-7, 6)))
        False
        >>> Line(P(0, 0), P(0, 5)).perpendicularTo(Line(P(-5, 0), P(5, 0)))
        True
        >>> Line(P(0, 5), P(0, 0)).perpendicularTo(Line(P(-5, 0), P(5, 0)))
        True
        >>> Line(P(4, 4), P(4, 4)).perpendicularTo(Line(P(-5, 0), P(5, 1)))
        Traceback (most recent call last):
          ...
        ValueError: Cannot determine perpendicularity for degenerate lines!
        """

        selfSlope = self.slope()
        otherSlope = other.slope()

        if selfSlope is None or otherSlope is None:
            raise ValueError(
                "Cannot determine perpendicularity for degenerate lines!")

        try:
            r = mathutilities.closeEnough(selfSlope, -1 / otherSlope)
        except ZeroDivisionError:
            r = abs(selfSlope) == float("+inf")

        return r

    def piece(self, t1, t2):
        """
        Creates a new Line object which maps t1 to t2 in the original to 0 to 1
        in the new.
        
        Returns two things: the new Line, and an anonymous function which
        maps an old t-value to a new u-value.
        
        >>> P = point.Point
        >>> L = Line(P(1, 1), P(5, 9))
        >>> print(L)
        Line from (1, 1) to (5, 9)
        >>> L2, func = L.piece(0.25, 0.75)
        >>> print(L2)
        Line from (2.0, 3.0) to (4.0, 7.0)
        >>> func(0.25), func(0.5), func(0.75)
        (0.0, 0.5, 1.0)
        """

        L = type(self)(self.pointFromParametricValue(t1),
                       self.pointFromParametricValue(t2))

        return L, lambda x: ((x - t1) / (t2 - t1))

    def pointFromParametricValue(self, t):
        """
        Given a parametric value, returns a Point on self at that value.
        
        >>> P = point.Point
        >>> L = Line(P(1, 1), P(5, 9))
        >>> print(L.pointFromParametricValue(0.0))
        (1.0, 1.0)
        >>> print(L.pointFromParametricValue(1.0))
        (5.0, 9.0)
        >>> print(L.pointFromParametricValue(-2.0))
        (-7.0, -15.0)
        >>> print(L.pointFromParametricValue(0.5))
        (3.0, 5.0)
        """

        return (self.p1 * (1 - t)) + (self.p2 * t)

    def rotated(self, angleInDegrees=0):
        """
        Returns a new Line rotated by the specified angle (measured counter-
        clockwise) about the origin.
        
        >>> P = point.Point
        >>> L = Line(P(3, -2), P(4, 4))
        >>> print(L.rotated(90))
        Line from (2.0, 3.0) to (-4.0, 4.0)
        """

        return type(self)(self.p1.rotated(angleInDegrees),
                          self.p2.rotated(angleInDegrees))

    def rotatedAbout(self, about, angleInDegrees=0):
        """
        Returns a new Line rotated by the specified angle (measured counter-
        clockwise) about the specified point.
        
        >>> P = point.Point
        >>> L = Line(P(3, -2), P(4, 4))
        >>> print(L.rotatedAbout(P(-3, 3), 90))
        Line from (2.0, 9.0) to (-4.0, 10.0)
        """

        return type(self)(self.p1.rotatedAbout(about, angleInDegrees),
                          self.p2.rotatedAbout(about, angleInDegrees))

    def slope(self):
        """
        Returns the slope of the Line (potentially a signed infinity, where the
        sign reflects a movement from self.p1 to self.p2).
        
        If the Line is degenerate, the slope will be returned as None, but no
        error will be raised.
        
        >>> P = point.Point
        >>> print(Line(P(2, 4), P(1, 2)).slope())
        2.0
        >>> print(Line(P(1, 2), P(2, 4)).slope())
        2.0
        >>> print(Line(P(2, 4), P(2, 2)).slope())
        -inf
        >>> print(Line(P(2, -4), P(2, 2)).slope())
        inf
        >>> print(Line(P(2, -4), P(2, -4)).slope())
        None
        """

        if self.isDegenerate():
            return None

        if mathutilities.closeEnough(self.p1.x, self.p2.x):
            if self.p2.y >= self.p1.y:
                return float("+inf")

            return float("-inf")

        return (self.p2.y - self.p1.y) / (self.p2.x - self.p1.x)
Пример #13
0
    def _makePt(x, y):
        from fontio3.fontmath import point

        return point.Point(x, y)
Пример #14
0
class Triangle(object, metaclass=simplemeta.FontDataMetaclass):
    """
    Objects representing triangles. These are simple objects with three
    attributes, p1, p2, and p3, representing the vertices.
    """

    #
    # Class definition variables
    #

    attrSpec = dict(p1=dict(attr_followsprotocol=True,
                            attr_initfunc=(lambda: point.Point(0, 0))),
                    p2=dict(attr_followsprotocol=True,
                            attr_initfunc=(lambda: point.Point(0, 0))),
                    p3=dict(attr_followsprotocol=True,
                            attr_initfunc=(lambda: point.Point(0, 0))))

    #
    # Special methods
    #

    def __str__(self):
        """
        Returns a string representation of the Triangle.
        
        >>> P = point.Point
        >>> print(Triangle(P(3, 5), P(0, 1), P(-1, 3)))
        Triangle with vertices (3, 5), (0, 1), and (-1, 3)
        """

        return ("Triangle with vertices %s, %s, and %s" %
                (self.p1, self.p2, self.p3))

    #
    # Public methods
    #

    def area(self):
        """
        Returns the area of the Triangle. Since we know the three sides we use
        Heron's formula.
        
        >>> P = point.Point
        >>> Triangle(P(3, 5), P(0, 1), P(-1, 3)).area()
        5.0
        >>> print(Triangle(P(-10, 0), P(0, 0.000001), P(10, 0)).area())
        9.973764735866506e-06
        """

        Line = line.Line
        line12 = Line(self.p1, self.p2)
        line23 = Line(self.p2, self.p3)
        line31 = Line(self.p3, self.p1)
        a = line12.length()
        b = line23.length()
        c = line31.length()
        s = (a + b + c) / 2
        radicand = s * (s - a) * (s - b) * (s - c)

        if radicand >= 0:
            return math.sqrt(radicand)

        return 0

    def center(self):
        """
        Returns a Point representing the center of the Triangle.
        
        >>> P = point.Point
        >>> print(Triangle(P(3, 5), P(0, 1), P(-1, 3)).center())
        (0.6666666666666667, 3.0)
        >>> print(Triangle(P(-10, 0), P(0, 0.000001), P(10, 0)).center())
        (0.0, 5e-07)
        """

        if self.isDegenerate():
            minX = min(self.p1.x, self.p2.x, self.p3.x)
            minY = min(self.p1.y, self.p2.y, self.p3.y)
            maxX = max(self.p1.x, self.p2.x, self.p3.x)
            maxY = max(self.p1.y, self.p2.y, self.p3.y)

            L = line.Line(point.Point(minX, minY), point.Point(maxX, maxY))

            return L.midpoint()

        Line = line.Line
        line12 = Line(self.p1, self.p2)
        line23 = Line(self.p2, self.p3)
        testLine1 = Line(self.p3, line12.midpoint())
        testLine2 = Line(self.p1, line23.midpoint())
        return testLine1.intersection(testLine2)

    def isDegenerate(self, delta=1.0e-5):
        """
        Returns True if the Triangle is degenerate (that is, if its area is
        essentially zero).
        
        >>> P = point.Point
        >>> Triangle(P(3, 5), P(0, 1), P(-1, 3)).isDegenerate()
        False
        >>> Triangle(P(3, 5), P(3.00000001, 5), P(-1, 3)).isDegenerate()
        True
        """

        return mathutilities.zeroEnough(self.area(), delta=delta)

    def isRightTriangle(self):
        """
        Returns True if the Triangle is a right triangle. Will always return
        False for a degenerate Triangle.
        
        >>> P = point.Point
        >>> Triangle(P(3, 5), P(0, 1), P(-1, 3)).isRightTriangle()
        True
        >>> Triangle(P(0, 0), P(3, 5), P(-3, 5)).isRightTriangle()
        False
        >>> Triangle(P(-10, 0), P(0, 0.0000001), P(10, 0)).isRightTriangle()
        False
        """

        if self.isDegenerate():
            return False

        Line = line.Line
        line12 = Line(self.p1, self.p2)
        line23 = Line(self.p2, self.p3)
        line31 = Line(self.p3, self.p1)

        return any([
            line12.perpendicularTo(line23),
            line23.perpendicularTo(line31),
            line31.perpendicularTo(line12)
        ])

    def magnified(self, xScale=1, yScale=1):
        """
        Returns a new Triangle scaled as specified about the origin.
        
        >>> P = point.Point
        >>> T = Triangle(P(3, 5), P(0, 1), P(-1, 3))
        >>> print(T)
        Triangle with vertices (3, 5), (0, 1), and (-1, 3)
        >>> print(T.magnified(2.5, -0.5))
        Triangle with vertices (7.5, -2.5), (0.0, -0.5), and (-2.5, -1.5)
        """

        return type(self)(self.p1.magnified(xScale, yScale),
                          self.p2.magnified(xScale, yScale),
                          self.p3.magnified(xScale, yScale))

    def magnifiedAbout(self, about, xScale=1, yScale=1):
        """
        Returns a new Triangle scaled as specified about a specified point.
        
        >>> P = point.Point
        >>> T = Triangle(P(3, 5), P(0, 1), P(-1, 3))
        >>> print(T)
        Triangle with vertices (3, 5), (0, 1), and (-1, 3)
        >>> print(T.magnifiedAbout(P(1, -1), xScale=1.75, yScale=-0.5))
        Triangle with vertices (4.5, -4.0), (-0.75, -2.0), and (-2.5, -3.0)
        """

        return type(self)(self.p1.magnifiedAbout(about, xScale, yScale),
                          self.p2.magnifiedAbout(about, xScale, yScale),
                          self.p3.magnifiedAbout(about, xScale, yScale))

    def moved(self, deltaX=0, deltaY=0):
        """
        Returns a new Triangle moved by the specified amount.
        
        >>> P = point.Point
        >>> T = Triangle(P(3, 5), P(0, 1), P(-1, 3))
        >>> print(T)
        Triangle with vertices (3, 5), (0, 1), and (-1, 3)
        >>> print(T.moved(-4, 4))
        Triangle with vertices (-1, 9), (-4, 5), and (-5, 7)
        """

        return type(self)(self.p1.moved(deltaX, deltaY),
                          self.p2.moved(deltaX, deltaY),
                          self.p3.moved(deltaX, deltaY))

    def perimeter(self):
        """
        Returns the perimeter of the Triangle.
        
        >>> P = point.Point
        >>> Triangle(P(3, 5), P(0, 1), P(-1, 3)).perimeter()
        11.70820393249937
        """

        Line = line.Line

        return (Line(self.p1, self.p2).length() +
                Line(self.p2, self.p3).length() +
                Line(self.p3, self.p1).length())

    def rotated(self, angleInDegrees=0):
        """
        Returns a new Triangle rotated by the specified angle (measured
        counter-clockwise) about the origin.
        
        >>> P = point.Point
        >>> T = Triangle(P(3, 5), P(0, 1), P(-1, 3))
        >>> print(T)
        Triangle with vertices (3, 5), (0, 1), and (-1, 3)
        >>> print(T.rotated(90))
        Triangle with vertices (-5.0, 3.0), (-1.0, 0.0), and (-3.0, -1.0)
        """

        return type(self)(self.p1.rotated(angleInDegrees),
                          self.p2.rotated(angleInDegrees),
                          self.p3.rotated(angleInDegrees))

    def rotatedAbout(self, about, angleInDegrees=0):
        """
        Returns a new Triangle rotated by the specified angle (measured
        counter-clockwise) about the specified point.
        
        >>> P = point.Point
        >>> T = Triangle(P(3, 5), P(0, 1), P(-1, 3))
        >>> print(T)
        Triangle with vertices (3, 5), (0, 1), and (-1, 3)
        >>> print(T.rotatedAbout(P(-3, 3), 90))
        Triangle with vertices (-5.0, 9.0), (-1.0, 6.0), and (-3.0, 5.0)
        """

        return type(self)(self.p1.rotatedAbout(about, angleInDegrees),
                          self.p2.rotatedAbout(about, angleInDegrees),
                          self.p3.rotatedAbout(about, angleInDegrees))
Пример #15
0
class BSpline(object, metaclass=simplemeta.FontDataMetaclass):
    """
    """

    #
    # Class definition variables
    #

    attrSpec = dict(onCurve1=dict(attr_followsprotocol=True,
                                  attr_initfunc=(lambda: point.Point(0, 0))),
                    offCurve=dict(attr_followsprotocol=True),
                    onCurve2=dict(attr_followsprotocol=True,
                                  attr_initfunc=(lambda: point.Point(0, 0))))

    attrSorted = ('onCurve1', 'onCurve2', 'offCurve')

    #
    # Methods
    #

    def __str__(self):
        """
        Returns a nice string representation of the spline. This also works for
        degenerate splines.
        
        >>> P = point.Point
        >>> print(BSpline(P(4, -3), P(4, -3), None))
        Point at (4, -3)
        
        >>> print(BSpline(P(1, 3), P(5, 0), None))
        Line from (1, 3) to (5, 0)
        
        >>> print(BSpline(P(2, -3), P(4, 4), P(0, 1)))
        Curve from (2, -3) to (4, 4) with off-curve point (0, 1)
        """

        on1 = self.onCurve1
        on2 = self.onCurve2

        if self.offCurve is None:
            if on1.equalEnough(on2):
                return "Point at %s" % (on1, )

            return "Line from %s to %s" % (on1, on2)

        return ("Curve from %s to %s with off-curve point %s" %
                (on1, on2, self.offCurve))

    def _intersection_curve(self, other):
        """
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> c = BSpline(P(2, 3), P(4, 4), P(0, 1))
        >>> b._intersection_curve(c)
        (Point(x=4, y=4), Point(x=4, y=4))
        """
        ZE = mathutilities.zeroEnough
        CES = mathutilities.closeEnoughSpan
        A = self.onCurve1.x + self.onCurve2.x - (2 * self.offCurve.x)
        assert not ZE(A)
        B = 2 * (self.offCurve.x - self.onCurve1.x)
        C = self.onCurve1.x
        D = other.onCurve1.x + other.onCurve2.x - (2 * other.offCurve.x)
        E = 2 * (other.offCurve.x - other.onCurve1.x)
        F = other.onCurve1.x
        G = self.onCurve1.y + self.onCurve2.y - (2 * self.offCurve.y)
        assert not ZE(G)
        H = 2 * (self.offCurve.y - self.onCurve1.y)
        denom = B * G - A * H
        assert not ZE(denom)
        I = self.onCurve1.y
        J = other.onCurve1.y + other.onCurve2.y - (2 * other.offCurve.y)
        K = 2 * (other.offCurve.y - other.onCurve1.y)
        L = other.onCurve1.y

        # This is the full case, where we have to solve the following pair of
        # equations:
        #
        #   At^2 + Bt + C = Du^2 + Eu + F
        #   Gt^2 + Ht + I = Ju^2 + Ku + L
        #
        # Since we know both A and G are nonzero, we can multiply the first
        # equation by G and the second one by A, and then subtract the two
        # equations. Simplifying, we get this equation representing t in terms
        # of u:
        #
        #   t = (DG-AJ)/(BG-AH) u^2 +
        #       (EG-AK)/(BG-AH) u   +
        #       ((FG-AL)+(AI-CG))/(BG-AH)
        #
        # To simplify, we define the following three values:
        #
        #   M = (DG-AJ)/(BG-AH)
        #   N = (EG-AK)/(BG-AH)
        #   P = ((FG-AL)+(AI-CG))/(BG-AH)
        #
        # This results in the simpler equation:
        #
        #   t = Mu^2 + Nu + P
        #
        # This is then substituted back into the y-equation:
        #
        #   Gt^2 + Ht + I = Ju^2 + Ku + L
        #
        # Resulting in the final quartic:
        #
        #   (GM^2)u^4 +
        #   (2GMN)u^3 +
        #   (GN^2 + 2GMP + HM - J)u^2+
        #   (2GNP + HN - K)u +
        #   (GP^2 + HP + I - L) = 0

        M = (D * G - A * J) / denom
        N = (E * G - A * K) / denom
        P = ((F * G - A * L) + (A * I - C * G)) / denom

        roots = mathutilities.quartic(G * M * M, 2 * G * M * N,
                                      G * N * N + 2 * G * M * P + H * M - J,
                                      2 * G * N * P + H * N - K,
                                      G * P * P + H * P + I - L)

        v = []

        for u in roots:
            if not ZE(u.imag):
                continue

            u = u.real

            if CES(u):
                # a possible solution
                t = M * u * u + N * u + P

                if CES(t):
                    # an actual solution
                    v.append(other.pointFromParametricValue(u))

        return tuple(v)

    def _intersection_curve_xspecial(self, other):
        ZE = mathutilities.zeroEnough
        CES = mathutilities.closeEnoughSpan
        A = self.onCurve1.x + self.onCurve2.x - (2 * self.offCurve.x)
        assert ZE(A)
        B = 2 * (self.offCurve.x - self.onCurve1.x)
        C = self.onCurve1.x
        D = other.onCurve1.x + other.onCurve2.x - (2 * other.offCurve.x)
        E = 2 * (other.offCurve.x - other.onCurve1.x)
        F = other.onCurve1.x
        G = self.onCurve1.y + self.onCurve2.y - (2 * self.offCurve.y)
        assert not ZE(G)
        H = 2 * (self.offCurve.y - self.onCurve1.y)
        I = self.onCurve1.y
        J = other.onCurve1.y + other.onCurve2.y - (2 * other.offCurve.y)
        K = 2 * (other.offCurve.y - other.onCurve1.y)
        L = other.onCurve1.y

        # Since A is zero, we have the following simplified equation:
        #
        #   Bt + C = Du^2 + Eu + F
        #
        # This gives us a solution for t in terms of u:
        #
        #   t = (Du^2 + Eu + (F - C)) / B
        #
        # To simplify, we define the following three values:
        #
        #   M = D / B
        #   N = E / B
        #   P = (F - C) / B
        #
        # This results in the simpler equation:
        #
        #   t = Mu^2 + Nu + P
        #
        # This is then substituted back into the y-equation:
        #
        #   Gt^2 + Ht + I = Ju^2 + Ku + L
        #
        # Resulting in the final quartic:
        #
        #   (GM^2)u^4 +
        #   (2GMN)u^3 +
        #   (GN^2 + 2GMP + HM - J)u^2+
        #   (2GNP + HN - K)u +
        #   (GP^2 + HP + I - L) = 0

        M = D / B
        N = E / B
        P = (F - C) / B

        roots = mathutilities.quartic(G * M * M, 2 * G * M * N,
                                      G * N * N + 2 * G * M * P + H * M - J,
                                      2 * G * N * P + H * N - K,
                                      G * P * P + H * P + I - L)

        v = []

        for u in roots:
            if not ZE(u.imag):
                continue

            u = u.real

            if CES(u):
                # a possible solution
                t = M * u * u + N * u + P

                if CES(t):
                    # an actual solution
                    v.append(other.pointFromParametricValue(u))

        return tuple(v)

    def _intersection_curve_yspecial(self, other):
        ZE = mathutilities.zeroEnough
        CES = mathutilities.closeEnoughSpan
        A = self.onCurve1.x + self.onCurve2.x - (2 * self.offCurve.x)
        assert not ZE(A)
        B = 2 * (self.offCurve.x - self.onCurve1.x)
        C = self.onCurve1.x
        D = other.onCurve1.x + other.onCurve2.x - (2 * other.offCurve.x)
        E = 2 * (other.offCurve.x - other.onCurve1.x)
        F = other.onCurve1.x
        G = self.onCurve1.y + self.onCurve2.y - (2 * self.offCurve.y)
        assert ZE(G)
        H = 2 * (self.offCurve.y - self.onCurve1.y)
        I = self.onCurve1.y
        J = other.onCurve1.y + other.onCurve2.y - (2 * other.offCurve.y)
        K = 2 * (other.offCurve.y - other.onCurve1.y)
        L = other.onCurve1.y

        # Since G is zero, we have the following simplified equation:
        #
        #   Ht + I = Ju^2 + Ku + L
        #
        # This gives us a solution for t in terms of u:
        #
        #   t = (Ju^2 + Ku + (L - I)) / H
        #
        # To simplify, we define the following three values:
        #
        #   M = J / H
        #   N = K / H
        #   P = (L - I) / H
        #
        # This results in the simpler equation:
        #
        #   t = Mu^2 + Nu + P
        #
        # This is then substituted back into the x-equation:
        #
        #   At^2 + Bt + C = Du^2 + Eu + F
        #
        # Resulting in the final quartic:
        #
        #   (AM^2)u^4 +
        #   (2AMN)u^3 +
        #   (AN^2 + 2AMP + BM - D)u^2+
        #   (2ANP + BN - E)u +
        #   (AP^2 + BP + C - F) = 0

        M = J / H
        N = K / H
        P = (L - I) / H

        roots = mathutilities.quartic(A * M * M, 2 * A * M * N,
                                      A * N * N + 2 * A * M * P + B * M - D,
                                      2 * A * N * P + B * N - E,
                                      A * P * P + B * P + C - F)

        v = []

        for u in roots:
            if not ZE(u.imag):
                continue

            u = u.real

            if CES(u):
                # a possible solution
                t = M * u * u + N * u + P

                if CES(t):
                    # an actual solution
                    v.append(other.pointFromParametricValue(u))

        return tuple(v)

    def _intersection_line(self, theLine):
        """
        Utility function handling the line-curve intersection cases. Note that
        self.offCurve must not be None (that will have been filtered out in
        the higher-level code).
       
        >>> P = point.Point
        >>> a = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> b = line.Line(P(2, -3), P(4, -3))
        >>> c = line.Line(P(4, -3), P(4, -3))
        >>> d = line.Line(P(4, -3), P(6, 4))
        >>> e = line.Line(P(2, 0), P(4, 0))
        >>> a._intersection_line(c)
        ()
        
        >>> a._intersection_line(b)
        (Point(x=2.0, y=-3.0),)
        
        >>> a._intersection_line(d)      
        ()
        
        >>> a._intersection_line(e)      
        ()
        """

        assert self.offCurve is not None
        ZE = mathutilities.zeroEnough
        CES = mathutilities.closeEnoughSpan

        # Set up the coefficients for the x-equation:
        #     At + B = Cu^2 + Du + E
        # and the y-equation:
        #     Ft + G = Hu^2 + Iu + J

        A = theLine.p2.x - theLine.p1.x
        B = theLine.p1.x
        C = self.onCurve1.x + self.onCurve2.x - 2 * self.offCurve.x
        D = 2 * (self.offCurve.x - self.onCurve1.x)
        E = self.onCurve1.x

        F = theLine.p2.y - theLine.p1.y
        G = theLine.p1.y
        H = self.onCurve1.y + self.onCurve2.y - 2 * self.offCurve.y
        I = 2 * (self.offCurve.y - self.onCurve1.y)
        J = self.onCurve1.y

        if ZE(A) and ZE(F):

            # If both A and F are zero, the curve is degenerate (the two on-
            # curve points are essentially coincident). In this case, there is
            # only a single point intersection possible if either of the on-
            # curve points lies on the line.

            t = theLine.parametricValueFromPoint(self.onCurve1)
            return (() if t is None else (self.onCurve1.__copy__(), ))

        if ZE(A):

            # If A is zero and F is not, we solve the simpler quadratic:
            #     Cu^2 + Du + (E-B) = 0
            # and then plug that u-value into the y-equation to get t.

            root1, root2 = mathutilities.quadratic(C, D, E - B)

            if not ZE(root1.imag):
                return ()

            v = []

            for u in (root1, root2):
                if CES(u):
                    # a possible solution
                    t = (H * u * u + I * u + J - G) / F

                    if CES(t):
                        # an actual solution
                        v.append(theLine.pointFromParametricValue(t))

            return tuple(v)

        if ZE(F):

            # If F is zero and A is not, we solve the simpler quadratic:
            #     Hu^2 + Iu + (J-G) = 0
            # and then plug that u-value into the y-equation to get t.

            root1, root2 = mathutilities.quadratic(H, I, J - G)

            if not ZE(root1.imag):
                return ()

            v = []

            for u in (root1, root2):
                if CES(u):
                    # a possible solution
                    t = (C * u * u + D * u + E - B) / A

                    if CES(t):
                        # an actual solution
                        v.append(theLine.pointFromParametricValue(t))

            return tuple(v)

        # If we get here, both A and F are nonzero, so we solve the full
        # quadratic:
        #
        #    (CF-AH)u^2 + (DF-AI)u + (EF+AG-AJ-BF) = 0
        #
        # and then plug that u-value into the y-equation to get t.

        root1, root2 = mathutilities.quadratic(C * F - A * H, D * F - A * I,
                                               E * F + A * G - A * J - B * F)

        if not ZE(root1.imag):
            return ()

        v = []

        for u in (root1, root2):
            if CES(u):
                # a possible solution
                t = (C * u * u + D * u + E - B) / A

                if CES(t):
                    # an actual solution
                    v.append(theLine.pointFromParametricValue(t))

        return tuple(v)

    def area(self):
        """
        Analytical solution for the area bounded by the parabolic section and
        the straight line.
        
        >>> P = point.Point
        >>> round(BSpline(P(2, -3), P(4, 4), P(0, 1)).area(), 10)
        20.6666666667
        >>> P = point.Point
        
        >>> print(BSpline(P(2, -3), P(4, 4), None).area())
        0
        
        >>> print(BSpline(P(2, -3), P(4, 4), P(4, 4)).area())
        Traceback (most recent call last):
          ...
        ZeroDivisionError: division by zero
        
        >>> print(BSpline(P(2, -3), P(2, -3), P(3, 4)).area())
        0
        """

        if self.offCurve is None:
            return 0

        if self.onCurve1.equalEnough(self.onCurve2):
            return 0

        normalized = self.moved(-self.onCurve1.x, -self.onCurve1.y)
        x1, y1 = normalized.offCurve.x, normalized.offCurve.y
        x2, y2 = normalized.onCurve2.x, normalized.onCurve2.y
        slopeDenom = x2 * (y2 - 2 * y1) - y2 * (x2 - 2 * x1)
        # assert slopeDenom, "Zero slope denominator in BSpline.area!"
        tangentT = (y2 * x1 - x2 * y1) / slopeDenom
        tangentPoint = normalized.pointFromParametricValue(tangentT)

        t = triangle.Triangle(self.onCurve1, tangentPoint, self.onCurve2)

        return 4 * t.area() / 3

    def bounds(self):
        """
        Returns a Rectangle representing the actual curve bounds. This might be
        different than the extrema.
        
        >>> P = point.Point
        >>> print(BSpline(P(2, -3), P(4, 4), P(0, 1)).extrema())
        Minimum X = 0, Minimum Y = -3, Maximum X = 4, Maximum Y = 4
        
        >>> print(BSpline(P(2, -3), P(4, 4), P(0, 1)).bounds())
        Minimum X = 1.3333333333333335, Minimum Y = -3, Maximum X = 4, Maximum Y = 4
        
        >>> print(BSpline(P(2, -3), P(4, 4), None).bounds())
        Minimum X = 2, Minimum Y = -3, Maximum X = 4, Maximum Y = 4
        
        >>> print(BSpline(P(4, 4), P(4, 4), P(2, 4)).bounds())
        Minimum X = 4, Minimum Y = 4, Maximum X = 4, Maximum Y = 4
        
        >>> print(BSpline(P(2, 3.1), P(2, 3.11), P(2,3.105)).bounds())
        Minimum X = 2, Minimum Y = 3.1, Maximum X = 2, Maximum Y = 3.11
        
        >>> print(BSpline(P(10, 5.1), P(12, 3.11), P(-2,1.105)).bounds())
        Minimum X = 4.461538461538462, Minimum Y = 2.439995833333333, Maximum X = 12, Maximum Y = 5.1
        """

        # We determine the bounds in x by differentiating the curve and finding
        # out where the slope zeroes out. Let a = self.onCurve1.x,
        # b = self.onCurve2.x, and c = self.offCurve.x. The equation for the
        # curve in x is:
        #
        # x = a(1-t)^2 + 2ct(1-t) + bt^2
        #
        # Refactoring this into terms involving t, we get:
        #
        # x = (a+b-2c)t^2 + (2c-2a)t + a
        #
        # Differentiating this with respect to t gets us:
        #
        # x' = 2(a+b-2c)t + (2c-2a)
        #
        # When x' is zero the curve is vertical. Solving, we get:
        #
        # t = (a-c)/(a+b-2c)
        #
        # If the denominator is zero then there are no vertical tangents. If t
        # is not in [0,1] then there are no vertical tangents. In both of these
        # cases the endpoints will provide the bounds.
        #
        # If, on the other hand, t exists and is in [0,1] then we need to plug
        # that value for t back into the original equation to get the
        # corresponding value of x.
        #
        # The same logic applies, mutatis mutandis, to y.

        extremaRect = self.extrema(True)

        if self.offCurve is None:
            return extremaRect

        if self.onCurve1.equalEnough(self.onCurve2):
            return extremaRect

        xMin, yMin, xMax, yMax = extremaRect.asList()
        a = self.onCurve1.x
        b = self.onCurve2.x
        c = self.offCurve.x
        denom = a + b - (2 * c)

        if denom:
            t = (a - c) / denom

            if mathutilities.closeEnoughSpan(t):
                p = self.pointFromParametricValue(t)
                xMin = min(xMin, p.x)
                xMax = max(xMax, p.x)

        a = self.onCurve1.y
        b = self.onCurve2.y
        c = self.offCurve.y
        denom = a + b - (2 * c)

        if denom:
            t = (a - c) / denom

            if mathutilities.closeEnoughSpan(t):
                p = self.pointFromParametricValue(t)
                yMin = min(yMin, p.y)
                yMax = max(yMax, p.y)

        return rectangle.Rectangle(xMin, yMin, xMax, yMax)

    def distanceToPoint(self, p):
        """
        Returns an unsigned distance from the point to the nearest position on
        the curve.
        
        >>> P = point.Point
        >>> obj = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(obj.distanceToPoint(P(3, 3)))
        0.10068277482538779
        >>> print(BSpline(P(2, -3), P(4, 4), None).distanceToPoint(P(2,-3)))
        0.0
        >>> print(BSpline(P(4, 4), P(4, 4), P(2, 4)).distanceToPoint(P(2,-3)))
        7.280109889280518
        """

        if self.onCurve1.equalEnough(self.onCurve2):
            return p.distanceFrom(self.onCurve1)

        if self.offCurve is None:
            return line.Line(self.onCurve1, self.onCurve2).distanceToPoint(p)

        a = self.onCurve1.x
        b = self.onCurve2.x
        c = self.offCurve.x
        d = self.onCurve1.y
        e = self.onCurve2.y
        f = self.offCurve.y
        A = a + b - 2 * c
        B = c - a
        C = d + e - 2 * f
        D = f - d
        E = a - p.x
        F = d - p.y

        roots = mathutilities.cubic(
            4 * (A * A + C * C), 12 * (A * B + C * D),
            4 * (2 * B * B + 2 * D * D + A * E + C * F), 4 * (B * E + D * F))

        ZE = mathutilities.zeroEnough

        v = [
            p.distanceFrom(self.pointFromParametricValue(root))
            for root in roots if ZE(root.imag)
        ]

        return utilities.safeMin(v)

    def extrema(self, excludeOffCurve=False):
        """
        Returns a Rectangle representing the extrema (based on the actual
        coordinates, and not the curve itself). Normally the off-curve point is
        included in the calculations; to exclude it, set the excludeOffCurve
        flag to True.
        
        Note that you can check whether one or more extreme points are off-
        curve by comparing the results of this method with True and False as
        the flags -- if the resulting Rectangles differ, then at least one of
        the extreme points is off-curve.
        
        >>> P = point.Point
        >>> print(BSpline(P(2, -3), P(4, 4), P(0, 1)).extrema())
        Minimum X = 0, Minimum Y = -3, Maximum X = 4, Maximum Y = 4
        >>> print(BSpline(P(2, -3), P(4, 4), P(0, 1)).extrema(True))
        Minimum X = 2, Minimum Y = -3, Maximum X = 4, Maximum Y = 4
        """

        if self.offCurve is None or self.onCurve1.equalEnough(self.onCurve2):
            return rectangle.Rectangle.frompoints(self.onCurve1, self.onCurve2)

        if excludeOffCurve:
            return rectangle.Rectangle(min(self.onCurve1.x, self.onCurve2.x),
                                       min(self.onCurve1.y, self.onCurve2.y),
                                       max(self.onCurve1.x, self.onCurve2.x),
                                       max(self.onCurve1.y, self.onCurve2.y))

        return rectangle.Rectangle(
            min(self.onCurve1.x, self.onCurve2.x, self.offCurve.x),
            min(self.onCurve1.y, self.onCurve2.y, self.offCurve.y),
            max(self.onCurve1.x, self.onCurve2.x, self.offCurve.x),
            max(self.onCurve1.y, self.onCurve2.y, self.offCurve.y))

    def intersection(self, other):
        """
        Returns a tuple of objects representing the intersection of the two
        BSplines.
        
        >>> P = point.Point
        >>> b1 = BSpline(P(2, -3), P(4, 4), None)
        >>> b2 = BSpline(P(1, 0), P(4, -1), None)
        >>> for obj in b1.intersection(b2): print(obj)
        (2.6956521739130435, -0.5652173913043479)
        >>> for obj in b1.intersection(b2.moved(10)): print(obj)
        
        >>> b3 = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> for obj in b3.intersection(b2): print(obj)
        (1.345578690319357, -0.11519289677311899)
        
        >>> for obj in b2.intersection(b3): print(obj)
        (1.345578690319357, -0.11519289677311899)
        
        >>> for obj in b3.intersection(b2.moved(10)): print(obj)
        
        >>> for obj in b3.intersection(b1.moved(-1)): print(obj)
        (2.6484772346100725, 2.7696703211352536)
        (1.4424318562990182, -1.4514885029534366)
        
        >>> b4 = BSpline(P(-2, 0), P(2, 0), P(0, 2))
        >>> for obj in b4.intersection(b3): print(obj)
        (1.4334792522721553, 0.4862843083263152)
        
        >>> b5 = BSpline(P(0, 3), P(0, -3), P(3, 0))
        >>> for obj in b5.intersection(b3): print(obj)
        (1.448865608003318, 0.553901030852897)
        (1.3583316809062103, -0.92195982264009)
        
        >>> b6 = BSpline(P(2, -3), P(2, -3), None)
        >>> b7 = BSpline(P(1, 0), P(4, -1), None)
        >>> b8 = BSpline(P(0, 0), P(10, 10), P(5,5))
        >>> b9 = BSpline(P(2.1, 1), P(20, 20), P(5,5))
        >>> b6.intersection(b7)
        ()
        >>> b7.intersection(b6)
        ()
        >>> b8.intersection(b9)
        ()
        """

        if self.onCurve1.equalEnough(self.onCurve2):
            t = other.parametricValueFromPoint(self.onCurve1)
            return (() if t is None else (self.onCurve1, ))

        if other.onCurve1.equalEnough(other.onCurve2):
            t = self.parametricValueFromPoint(other.onCurve1)
            return (() if t is None else (other.onCurve1, ))

        self = self.normalized()
        other = other.normalized()
        CEXY = mathutilities.closeEnoughXY

        if self.offCurve is None and other.offCurve is None:
            # actually a line intersection case
            L1 = line.Line(self.onCurve1, self.onCurve2)
            L2 = line.Line(other.onCurve1, other.onCurve2)
            isect = L1.intersection(L2)
            return (() if isect is None else (isect, ))

        if self.offCurve is None or other.offCurve is None:

            # one is a line and one is a curve. check endpoints first.

            if (CEXY(self.onCurve1, other.onCurve1)
                    or CEXY(self.onCurve1, other.onCurve2)):

                return (self.onCurve1.__copy__(), )

            if (CEXY(self.onCurve2, other.onCurve1)
                    or CEXY(self.onCurve2, other.onCurve2)):

                return (self.onCurve2.__copy__(), )

            if self.offCurve is None:
                # self is line and other is curve; from 0-2 intersection points
                return other._intersection_line(
                    line.Line(self.onCurve1, self.onCurve2))

            # other is line and self is curve; from 0-2 intersection points
            return self._intersection_line(
                line.Line(other.onCurve1, other.onCurve2))

        if CEXY(self.offCurve, other.offCurve):
            # If the two off-curve points coincide, then we check to see if
            # other's start and end points lie on self. If they do, the curves
            # overlap.

            tStart = self.parametricValueFromPoint(other.onCurve1)

            if tStart is not None:
                tEnd = self.parametricValueFromPoint(other.onCurve2)

                if tEnd is not None:
                    return (self.piece(max(0, tStart), min(1, tEnd))[0],
                            )  # don't need the function

        if CEXY(self.onCurve1, other.onCurve1) or CEXY(self.onCurve1,
                                                       other.onCurve2):
            return (self.onCurve1.__copy__(), )

        if CEXY(self.onCurve2, other.onCurve1) or CEXY(self.onCurve2,
                                                       other.onCurve2):
            return (self.onCurve2.__copy__(), )

        # At this point we know it's a full curve/curve intersection, so the
        # possible results are 0-2 points

        ZE = mathutilities.zeroEnough
        A = self.onCurve1.x + self.onCurve2.x - (2 * self.offCurve.x)
        B = 2 * (self.offCurve.x - self.onCurve1.x)
        G = self.onCurve1.y + self.onCurve2.y - (2 * self.offCurve.y)
        H = 2 * (self.offCurve.y - self.onCurve1.y)

        assert not (ZE(A) and ZE(G))  # linear case already handled

        if ZE(A):
            return self._intersection_curve_xspecial(other)

        if ZE(G):
            return self._intersection_curve_yspecial(other)

        if ZE(B * G - A * H):
            return other.intersection(self)

        return self._intersection_curve(other)

    def magnified(self, xScale=1, yScale=1):
        """
        Returns a new BSpline scaled as specified about the origin.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b)
        Curve from (2, -3) to (4, 4) with off-curve point (0, 1)
        >>> print(b.magnified(-2.5, 0.75))
        Curve from (-5.0, -2.25) to (-10.0, 3.0) with off-curve point (-0.0, 0.75)
        >>> c = BSpline(P(2, -3), P(4, 4), None)
        >>> print(c.magnified(5, -5))
        Line from (10, 15) to (20, -20)
        """

        if self.offCurve is None:
            return type(self)(self.onCurve1.magnified(xScale, yScale),
                              self.onCurve2.magnified(xScale, yScale), None)

        return type(self)(self.onCurve1.magnified(xScale, yScale),
                          self.onCurve2.magnified(xScale, yScale),
                          self.offCurve.magnified(xScale, yScale))

    def magnifiedAbout(self, about, xScale=1, yScale=1):
        """
        Returns a new BSpline scaled as specified about a specified point.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b)
        Curve from (2, -3) to (4, 4) with off-curve point (0, 1)
        >>> print(b.magnifiedAbout(P(1, -1), -2.5, 0.75))
        Curve from (-1.5, -2.5) to (-6.5, 2.75) with off-curve point (3.5, 0.5)
        >>> c = BSpline(P(2, -3), P(4, 4), None)
        >>> print(c.magnifiedAbout(P(-1,3), 5, -5))
        Line from (14, 33) to (24, -2)
        """

        if self.offCurve is None:
            return type(self)(
                self.onCurve1.magnifiedAbout(about, xScale, yScale),
                self.onCurve2.magnifiedAbout(about, xScale, yScale), None)

        return type(self)(self.onCurve1.magnifiedAbout(about, xScale, yScale),
                          self.onCurve2.magnifiedAbout(about, xScale, yScale),
                          self.offCurve.magnifiedAbout(about, xScale, yScale))

    def moved(self, deltaX=0, deltaY=0):
        """
        Returns a new BSpline moved by the specified amount.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b)
        Curve from (2, -3) to (4, 4) with off-curve point (0, 1)
        >>> print(b.moved(-2, 3))
        Curve from (0, 0) to (2, 7) with off-curve point (-2, 4)
        """

        if self.offCurve is None:
            return type(self)(self.onCurve1.moved(deltaX, deltaY),
                              self.onCurve2.moved(deltaX, deltaY), None)

        return type(self)(self.onCurve1.moved(deltaX, deltaY),
                          self.onCurve2.moved(deltaX, deltaY),
                          self.offCurve.moved(deltaX, deltaY))

    def normal(self):
        """
        Returns a new BSpline of unit length, starting from the point on the
        curve corresponding to a t-value of 0.5, and moving in a direction
        orthogonal to the tangent at that point, outwards (i.e. away from the
        line from self.onCurve1 to self.onCurve2).
        
        >>> P = point.Point
        >>> print(BSpline(P(2, -3), P(4, 4), P(0, 1)).normal())
        Line from (1.5, 0.75) to (0.5384760523591766, 1.0247211278973771)
        """

        startPt = self.pointFromParametricValue(0.5)
        refLine = line.Line(self.onCurve1, self.onCurve2)
        tProj = refLine.parametricValueFromProjectedPoint(startPt)
        tProjPt = refLine.pointFromParametricValue(tProj)
        unnormEndPt = (startPt * 2) - tProjPt
        unnormLine = line.Line(startPt, unnormEndPt)
        return unnormLine.normalized()

    def normalized(self):
        """
        If self.offCurve is None, or if self.offCurve is not None and lies
        legitimately off the line from self.onCurve1 to self.onCurve2, then
        self is returned. Otherwise, a new BSpline is created with the off-
        curve point set appropriately.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b)
        Curve from (2, -3) to (4, 4) with off-curve point (0, 1)
        >>> print(b.normalized())
        Curve from (2, -3) to (4, 4) with off-curve point (0, 1)
        
        >>> b = BSpline(P(0, 0), P(10, 10), P(5, 5))
        >>> print(b)
        Curve from (0, 0) to (10, 10) with off-curve point (5, 5)
        >>> print(b.normalized())
        Line from (0, 0) to (10, 10)
        
        We also need to normalize the cases where the off-curve point is
        co-linear with the two on-curve points, but has a parametric value
        less than zero or greater than one:
        
        >>> print(BSpline(P(5, 5), P(10, 10), P(0, 0)).normalized())
        Line from (0, 0) to (10, 10)
        >>> print(BSpline(P(0,0), P(10, 10), P(5,5)).normalized())
        Line from (0, 0) to (10, 10)
        >>> print(BSpline(P(0,0), P(10, 10), P(9.0001,10.0001)).normalized())
        Curve from (0, 0) to (10, 10) with off-curve point (9.0001, 10.0001)
        """

        if self.offCurve is None or self.onCurve1.equalEnough(self.onCurve2):
            return self

        L = line.Line(self.onCurve1, self.onCurve2)
        t = L.parametricValueFromPoint(self.offCurve)

        if t is None:
            return self

        # If we get here, the supposed off-curve point is actually colinear
        # with the two on-curve points, so it's a straight line and needs to
        # reflect that fact.

        if mathutilities.closeEnoughSpan(t):
            return type(self)(self.onCurve1, self.onCurve2, None)

        if t < 0:
            return type(self)(self.offCurve, self.onCurve2, None)

        return type(self)(self.onCurve1, self.offCurve, None)

    def parametricValueFromPoint(self, p):
        """
        Given a Point p, find the parametric value t which corresponds to the
        point along the curve. Returns None if the point does not lie on the
        curve.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b.parametricValueFromPoint(b.pointFromParametricValue(0)))
        0
        >>> print(b.parametricValueFromPoint(b.pointFromParametricValue(1)))
        1
        >>> print(b.parametricValueFromPoint(b.pointFromParametricValue(0.5)))
        0.5
        
        >>> m = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b.parametricValueFromPoint(b.pointFromParametricValue(0)))
        0
        
        >>> b = BSpline(P(5, 5), P(10, 10), None)
        >>> print(b.parametricValueFromPoint(P(0, 0)))
        -1.0
        """

        if self.offCurve is None:
            theLine = line.Line(self.onCurve1, self.onCurve2)
            return theLine.parametricValueFromPoint(p)

        a = self.onCurve1.x + self.onCurve2.x - 2 * self.offCurve.x
        b = 2 * (self.offCurve.x - self.onCurve1.x)
        c = self.onCurve1.x - p.x

        if a:
            tx1, tx2 = mathutilities.quadratic(a, b, c)
        elif b:
            tx1 = tx2 = -c / b
        else:
            """           
            >>> d = BSpline(P(1, 5), P(1, 10), P(1,-5))
            >>> print(d.parametricValueFromPoint(P(0, 0)))
            None
            >>> e = BSpline(P(-9, 5), P(1, 10), P(-1,-5))
            >>> print(e.parametricValueFromPoint(P(0, 0)))
            None
            """
            return (None if c else 0)

        if tx1.imag:
            return None

        a = self.onCurve1.y + self.onCurve2.y - 2 * self.offCurve.y
        b = 2 * (self.offCurve.y - self.onCurve1.y)
        c = self.onCurve1.y - p.y

        if a:
            ty1, ty2 = mathutilities.quadratic(a, b, c)
        elif b:
            ty1 = ty2 = -c / b
        else:
            """
            >>> c = BSpline(P(5, 1), P(10, 1), P(-5,1))
            >>> print(c.parametricValueFromPoint(P(0, 0)))
            None
            """
            return (None if c else 0)

        CE = mathutilities.closeEnough

        if CE(tx1, ty1) or CE(tx1, ty2):
            return tx1

        if CE(tx2, ty1) or CE(tx2, ty2):
            return tx2

        return None

    def piece(self, t1, t2):
        """
        Create a new b-spline mapping the original parametric values in the
        range t1 to t2 into new values from 0 to 1.

        Returns two things: the new b-spline, and an anonymous function which
        maps an old t-value (i.e. a value in the range from t1 through t2) onto
        a new u-value from 0 through 1.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> bNew, f = b.piece(0.25, 0.75)
        >>> print(bNew)
        Curve from (1.375, -1.0625) to (2.375, 2.4375) with off-curve point (1.125, 0.8125)
        >>> for t in (0.0, 0.25, 0.5, 0.75, 1.0):
        ...   tNew = f(t)
        ...   print(b.pointFromParametricValue(t), bNew.pointFromParametricValue(tNew))
        (2.0, -3.0) (2.0, -3.0)
        (1.375, -1.0625) (1.375, -1.0625)
        (1.5, 0.75) (1.5, 0.75)
        (2.375, 2.4375) (2.375, 2.4375)
        (4.0, 4.0) (4.0, 4.0)
        >>> c = BSpline(P(2, -3), P(4, 4), None)
        >>> c.piece(0,10)[0].pprint()
        onCurve1:
          0: 2.0
          1: -3.0
        onCurve2:
          0: 22.0
          1: 67.0
        offCurve: (no data)
        
        >>> c.piece(100,-10)[0].pprint()
        onCurve1:
          0: -18.0
          1: -73.0
        onCurve2:
          0: 202.0
          1: 697.0
        offCurve: (no data)
        """

        if t1 > t2:
            t1, t2 = t2, t1

        newP0 = self.pointFromParametricValue(t1)
        newP2 = self.pointFromParametricValue(t2)

        if self.offCurve is None:
            # linear case
            newSpline = BSpline(newP0, newP2)

        else:
            # quadratic case
            newP1 = self.pointFromParametricValue((t1 + t2) / 2)
            newP1x = 2 * (newP1.x - 0.25 * (newP0.x + newP2.x))
            newP1y = 2 * (newP1.y - 0.25 * (newP0.y + newP2.y))
            newSpline = BSpline(newP0, newP2, point.Point(newP1x, newP1y))

        return newSpline, lambda x: ((x - t1) / (t2 - t1))

    def pointFromParametricValue(self, t):
        """
        Given a parametric value t, where t=0 maps to self.onCurve1 and t=1
        maps to self.onCurve2, returns a Point representing that value.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b.pointFromParametricValue(0))
        (2, -3)
        >>> print(b.pointFromParametricValue(1))
        (4, 4)
        >>> print(b.pointFromParametricValue(0.5))
        (1.5, 0.75)
        """

        if self.offCurve is None:
            # linear case
            x = (t * self.onCurve2.x) + ((1.0 - t) * self.onCurve1.x)
            y = (t * self.onCurve2.y) + ((1.0 - t) * self.onCurve1.y)

        else:
            # quadratic case
            factor0 = (1 - t)**2
            factor1 = 2 * t * (1 - t)
            factor2 = t * t

            x = ((factor0 * self.onCurve1.x) + (factor1 * self.offCurve.x) +
                 (factor2 * self.onCurve2.x))

            y = ((factor0 * self.onCurve1.y) + (factor1 * self.offCurve.y) +
                 (factor2 * self.onCurve2.y))

        return point.Point(x, y)

    def rotated(self, angleInDegrees=0):
        """
        Returns a new BSpline scaled as specified about the origin.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b)
        Curve from (2, -3) to (4, 4) with off-curve point (0, 1)
        >>> print(b.rotated(90))
        Curve from (3.0, 2.0) to (-4.0, 4.0) with off-curve point (-1.0, 0.0)
        """

        if self.offCurve is None:
            return type(self)(self.onCurve1.rotated(angleInDegrees),
                              self.onCurve2.rotated(angleInDegrees), None)

        return type(self)(self.onCurve1.rotated(angleInDegrees),
                          self.onCurve2.rotated(angleInDegrees),
                          self.offCurve.rotated(angleInDegrees))

    def rotatedAbout(self, about, angleInDegrees=0):
        """
        Returns a new BSpline scaled as specified about a specified point.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b)
        Curve from (2, -3) to (4, 4) with off-curve point (0, 1)
        >>> print(b.rotatedAbout(P(1, -1), 90))
        Curve from (3.0, 0.0) to (-4.0, 2.0) with off-curve point (-1.0, -2.0)
        """

        if self.offCurve is None:
            return type(self)(
                self.onCurve1.rotatedAbout(about, angleInDegrees),
                self.onCurve2.rotatedAbout(about, angleInDegrees), None)

        return type(self)(self.onCurve1.rotatedAbout(about, angleInDegrees),
                          self.onCurve2.rotatedAbout(about, angleInDegrees),
                          self.offCurve.rotatedAbout(about, angleInDegrees))

    def slopeFromParametricValue(self, t):
        """
        Returns the slope of the curve at parametric value t.
        
        >>> P = point.Point
        >>> b = BSpline(P(2, -3), P(4, 4), P(0, 1))
        >>> print(b.slopeFromParametricValue(0))
        -2.0
        >>> print(b.slopeFromParametricValue(0.5))
        3.5
        >>> print(b.slopeFromParametricValue(1))
        0.75
        """

        # The slope is dy/dx, which can be expressed parametrically by:
        #
        # dy/dx = dy/dt divided by dx/dt
        #
        # Let a=onCurve1.x, b=offCurve.x, and c=onCurve2.x. The parametric
        # equation for x in terms of t is:
        #
        # x = a(1-t)^2 + 2bt(1-t) + ct^2
        #
        # Expanding and differentiating with respect to t, we get:
        #
        # dx/dt = 2(a+c-2b)t + 2(b-a)
        #
        # Using d, e, and f for analogous equations in y, we get:
        #
        # dy/dt = 2(d+f-2e)t + 2(e-d)
        #
        # Dividing dy/dt by dx/dt gives us the result.

        if self.offCurve is None:
            return line.Line(self.onCurve1, self.onCurve2).slope()

        a = self.onCurve1.x
        b = self.offCurve.x
        c = self.onCurve2.x
        denom = (a + c - 2 * b) * t + (b - a)

        if mathutilities.zeroEnough(denom):
            return float("+inf")

        d = self.onCurve1.y
        e = self.offCurve.y
        f = self.onCurve2.y
        numer = (d + f - 2 * e) * t + (e - d)

        return numer / denom

    def transitions(self, xStart, xStop, yCoord, leftToRight):
        """
        Finds the intersection of self and the horizontal line segment defined
        by xStart, xStop, and yCoord, then for each point in the intersection
        determines whether the curve passes by left-to-right or right-to-left
        when looked at in the specified leftToRight orientation. See the
        doctest examples below for a sense of how this works.
        
        >>> P = point.Point
        >>> b = BSpline(P(0, 0), P(10, 0), P(5, 20))
        >>> print(b.transitions(0, 10, 4, True))
        {False: 1, True: 1}
        
        >>> print(b.transitions(0, 10, 4, False))
        {False: 1, True: 1}
        
        >>> print(b.transitions(5, 10, 4, True))
        {False: 0, True: 1}
        
        >>> print(b.transitions(5, 10, 4, False))
        {False: 1, True: 0}
        
        >>> print(b.transitions(0, 5, 4, True))
        {False: 1, True: 0}
        
        >>> print(b.transitions(0, 5, 4, False))
        {False: 0, True: 1}
        
        Note that tangent points are not considered intersections for the
        purposes of this method:
        
        >>> b = BSpline(P(0, 0), P(7, -1), P(7, 1))
        >>> print(b.transitions(0, 7, 1/3, True))
        {False: 0, True: 0}
        """

        ray = type(self)(onCurve1=point.Point(xStart, yCoord),
                         onCurve2=point.Point(xStop, yCoord),
                         offCurve=None)

        sects = self.intersection(ray)
        r = {False: 0, True: 0}
        CE = mathutilities.closeEnough

        for p in sorted(sects, reverse=(not leftToRight)):
            if isinstance(p, line.Line):
                continue

            t = self.parametricValueFromPoint(p)
            p2 = self.pointFromParametricValue(t - 0.001)
            p3 = self.pointFromParametricValue(t + 0.001)

            if CE(p2.y, p3.y):
                continue

            if p2.y > p3.y:
                r[leftToRight] += 1
            else:
                r[not leftToRight] += 1

        return r
Пример #16
0
    def transitions(self, xStart, xStop, yCoord, leftToRight):
        """
        Finds the intersection of self and the horizontal line segment defined
        by xStart, xStop, and yCoord, then for each point in the intersection
        determines whether the curve passes by left-to-right or right-to-left
        when looked at in the specified leftToRight orientation. See the
        doctest examples below for a sense of how this works.
        
        >>> P = point.Point
        >>> b = BSpline(P(0, 0), P(10, 0), P(5, 20))
        >>> print(b.transitions(0, 10, 4, True))
        {False: 1, True: 1}
        
        >>> print(b.transitions(0, 10, 4, False))
        {False: 1, True: 1}
        
        >>> print(b.transitions(5, 10, 4, True))
        {False: 0, True: 1}
        
        >>> print(b.transitions(5, 10, 4, False))
        {False: 1, True: 0}
        
        >>> print(b.transitions(0, 5, 4, True))
        {False: 1, True: 0}
        
        >>> print(b.transitions(0, 5, 4, False))
        {False: 0, True: 1}
        
        Note that tangent points are not considered intersections for the
        purposes of this method:
        
        >>> b = BSpline(P(0, 0), P(7, -1), P(7, 1))
        >>> print(b.transitions(0, 7, 1/3, True))
        {False: 0, True: 0}
        """

        ray = type(self)(onCurve1=point.Point(xStart, yCoord),
                         onCurve2=point.Point(xStop, yCoord),
                         offCurve=None)

        sects = self.intersection(ray)
        r = {False: 0, True: 0}
        CE = mathutilities.closeEnough

        for p in sorted(sects, reverse=(not leftToRight)):
            if isinstance(p, line.Line):
                continue

            t = self.parametricValueFromPoint(p)
            p2 = self.pointFromParametricValue(t - 0.001)
            p3 = self.pointFromParametricValue(t + 0.001)

            if CE(p2.y, p3.y):
                continue

            if p2.y > p3.y:
                r[leftToRight] += 1
            else:
                r[not leftToRight] += 1

        return r