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 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 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 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 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 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 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 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 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 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 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 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 _validate(obj, **kwArgs): logger = kwArgs['logger'] r = True # check for empty contour if len(obj) < 1: logger.error(( 'V0950', (), "Contour is empty (no points).")) r = False # check for coincident adjacent points sawDups = sawBadDups = False for k, g in itertools.groupby(obj, key=tuple): v = list(g) if len(v) > 1: sawDups = True if len(set(x.onCurve for x in v)) > 1: sawBadDups = True if sawBadDups: logger.error(( 'V0295', (), "Contour has duplicate adjacent points of differing " "on-curve states.")) r = False elif sawDups: logger.warning(( 'W1111', (), "Contour has duplicate adjacent points.")) # check for coincident non-adjacent points and zero-length contours d = {} sawDups = sawBadDups = False for i, p in enumerate(obj): d.setdefault(tuple(p), set()).add(i) if len(d) == 1: logger.warning(( 'W1113', (), "Contour is degenerate (only a single (x, y) location).")) if len(d) == 2: logger.error(( 'V1013', (), "Contour is degenerate (only two (x,y) locations).")) for k, s in d.items(): if len(s) > 1: for a, b in itertools.permutations(s, 2): if abs(a - b) > 1: sawDups = True if obj[a].onCurve != obj[b].onCurve: sawBadDups = True if sawBadDups: logger.error(( 'V0297', (), "Contour has duplicate non-adjacent points of differing " "on-curve states.")) r = False elif sawDups: logger.warning(( 'V0296', (), "Contour has duplicate non-adjacent points.")) # check for on-curve points on the extrema extRectWith = obj.extrema(False) extRectWithout = obj.extrema(True) if extRectWith != extRectWithout: logger.warning(( 'W1112', (), "Not all extrema are marked with on-curve points.")) # check for internally-intersecting contours allSplines = list(obj.splineIterator()) allExtrema = [x.extrema() for x in allSplines] it = zip(allSplines, allExtrema) foundOverlap = False CE = mathutilities.closeEnough for (curve1, rect1), (curve2, rect2) in itertools.combinations(it, 2): # only bother with intersection check if extrema overlap if ( rect1.isEmpty() or rect2.isEmpty() or max(rect1.overlapDegrees(rect2)) > 0): for sectObj in curve1.intersection(curve2): if not isinstance(sectObj, point.Point): foundOverlap = True break t = curve1.parametricValueFromPoint(sectObj) u = curve2.parametricValueFromPoint(sectObj) if ( (t is not None) and (u is not None) and not ((CE(t, 0) or CE(t, 1)) and (CE(u, 0) or CE(u, 1)))): foundOverlap = True break if foundOverlap: break if foundOverlap: logger.warning(( 'E1111', (), "Contour intersects itself.")) # check for co-linear off-curve points for curve in allSplines: if curve.offCurve is not None: L = line.Line(curve.onCurve1, curve.onCurve2) t = L.parametricValueFromPoint(curve.offCurve) if t is not None: logger.warning(( 'V0309', (curve.offCurve,), "The off-curve point %s is co-linear with its two " "adjacent on-curve points.")) return r
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 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)