def curveIntersections(p1, p2, p3, p4, x1, y1, x2, y2): """ Computes intersection between a cubic spline and a line segment. Adapted from: https://www.particleincell.com/2013/cubic-line-intersection/ Takes four defcon points describing curve and four scalars describing line parameters. """ bx, by = x1 - x2, y2 - y1 m = x1 * (y1 - y2) + y1 * (x2 - x1) a, b, c, d = bezierTools.calcCubicParameters( (p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y), (p4.x, p4.y)) pc0 = by * a[0] + bx * a[1] pc1 = by * b[0] + bx * b[1] pc2 = by * c[0] + bx * c[1] pc3 = by * d[0] + bx * d[1] + m r = bezierTools.solveCubic(pc0, pc1, pc2, pc3) sol = [] for t in r: s0 = a[0] * t ** 3 + b[0] * t ** 2 + c[0] * t + d[0] s1 = a[1] * t ** 3 + b[1] * t ** 2 + c[1] * t + d[1] if (x2 - x1) != 0: s = (s0 - x1) / (x2 - x1) else: s = (s1 - y1) / (y2 - y1) if not (t < 0 or t > 1 or s < 0 or s > 1): sol.append((s0, s1, t)) return sol
def _tValueForPointOnCubicCurve(point, cubicCurve, isHorizontal=0): """ Finds a t value on a curve from a point. The points must be originaly be a point on the curve. This will only back trace the t value, needed to split the curve in parts """ pt1, pt2, pt3, pt4 = cubicCurve a, b, c, d = bezierTools.calcCubicParameters(pt1, pt2, pt3, pt4) solutions = bezierTools.solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal], d[isHorizontal] - point[isHorizontal]) solutions = [t for t in solutions if 0 <= t < 1] if not solutions and not isHorizontal: # can happen that a horizontal line doens intersect, try the vertical return _tValueForPointOnCubicCurve(point, (pt1, pt2, pt3, pt4), isHorizontal=1) if len(solutions) > 1: intersectionLenghts = {} for t in solutions: tp = _getCubicPoint(t, pt1, pt2, pt3, pt4) dist = _distance(tp, point) intersectionLenghts[dist] = t minDist = min(intersectionLenghts.keys()) solutions = [intersectionLenghts[minDist]] return solutions
def collectsPointsOnBezierCurve(pt1, pt2, pt3, pt4, tStep): """Adapted from calcCubicBounds in fontTools by Just van Rossum https://github.com/behdad/fonttools""" a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4) steps = [t / float(tStep) for t in xrange(tStep)] pointsWithT = [(pt1, 0)] for eachT in steps: pt = calcPointOnBezier(a, b, c, d, eachT) pointsWithT.append((pt, eachT)) pointsWithT.append((pt4, 1)) return pointsWithT
def convertCurveToArcs(pt1, pt2, pt3, pt4): aa, bb, cc, dd = calcCubicParameters((pt1.x, pt1.y), (pt2.x, pt2.y), (pt3.x, pt3.y), (pt4.x, pt4.y)) startT = 0 endT = 1 arcs = [] while startT != 1: center, startAngle, endAngle, radius, approxT = binaryArcApprox( pt1, pt2, pt3, pt4, startT, endT) startT = approxT arcs.append((center, startAngle, endAngle, radius, endT)) return arcs
def getExtremaForCubic(pt1, pt2, pt3, pt4, h=True, v=False): (ax, ay), (bx, by), c, d = calcCubicParameters(pt1, pt2, pt3, pt4) ax *= 3.0 ay *= 3.0 bx *= 2.0 by *= 2.0 h_roots = [] v_roots = [] if h: h_roots = [t for t in solveQuadratic(ay, by, c[1]) if 0 < t < 1] if v: v_roots = [t for t in solveQuadratic(ax, bx, c[0]) if 0 < t < 1] roots = h_roots + v_roots return [p[3] for p in splitCubicAtT(pt1, pt2, pt3, pt4, *roots)[:-1]]
def getExtremes(self, point=Tuple[int, ...]) -> list: # gets extremes of a cubic bezier curve pt1, pt2, pt3, pt4 = point (ax, ay), (bx, by), (cx, cy), _ = calcCubicParameters(pt1, pt2, pt3, pt4) ax3 = ax * 3.0 ay3 = ay * 3.0 bx2 = bx * 2.0 by2 = by * 2.0 collector: list = [] if self.horizontal: collector += [t for t in solveQuadratic(ax3, bx2, cx) if 0 <= t < 1] if self.vertical: collector += [t for t in solveQuadratic(ay3, by2, cy) if 0 <= t < 1] return sorted(collector)
def getExtremaForCubic(pt1, pt2, pt3, pt4, h=True, v=False): (ax, ay), (bx, by), c, d = calcCubicParameters(pt1, pt2, pt3, pt4) ax *= 3.0 ay *= 3.0 bx *= 2.0 by *= 2.0 points = [] vectors = [] if h: roots = [t for t in solveQuadratic(ay, by, c[1]) if 0 < t < 1] points, vectors = get_extrema_points_vectors(roots, pt1, pt2, pt3, pt4) if v: roots = [t for t in solveQuadratic(ax, bx, c[0]) if 0 < t < 1] v_points, v_vectors = get_extrema_points_vectors(roots, pt1, pt2, pt3, pt4) points += v_points vectors += v_vectors return points, vectors
def getExtremaForCubic(pt1, pt2, pt3, pt4, h=True, v=False): (ax, ay), (bx, by), c, d = calcCubicParameters(pt1, pt2, pt3, pt4) ax *= 3.0 ay *= 3.0 bx *= 2.0 by *= 2.0 points = [] vectors = [] if h: roots = [t for t in solveQuadratic(ay, by, c[1]) if 0 < t < 1] points, vectors = get_extrema_points_vectors(roots, pt1, pt2, pt3, pt4) if v: roots = [t for t in solveQuadratic(ax, bx, c[0]) if 0 < t < 1] v_points, v_vectors = get_extrema_points_vectors( roots, pt1, pt2, pt3, pt4) points += v_points vectors += v_vectors return points, vectors
def binaryArcApprox(pt1, pt2, pt3, pt4, startT, endT, prevStatus=False): assert endT <= 1, f'startT {startT}, endT {endT}' """ as soon as isGoodArc() returns True and then False, we stop and pick the True arc :) """ midT = lerp(startT, endT, .5) # curve parameters aa, bb, cc, dd = calcCubicParameters((pt1.x, pt1.y), (pt2.x, pt2.y), (pt3.x, pt3.y), (pt4.x, pt4.y)) # hypothetical arc points startPt = calcPointOnBezier(aa, bb, cc, dd, startT) midPt = calcPointOnBezier(aa, bb, cc, dd, midT) endPt = calcPointOnBezier(aa, bb, cc, dd, endT) # test points belowT = startT + (endT - startT) * .25 belowMidPt = calcPointOnBezier(aa, bb, cc, dd, belowT) aboveT = startT + (endT - startT) * .75 aboveMidPt = calcPointOnBezier(aa, bb, cc, dd, aboveT) center, startAngle, endAngle, radius = getCCenter(startPt, midPt, endPt) arcStatus, errors = isGoodArc(center, radius, belowMidPt, aboveMidPt) if arcStatus is True: if endT == 1: # the curve is finished, it is time to stop recursion return center, startAngle, endAngle, radius, endT else: # to the top! return binaryArcApprox(pt1, pt2, pt3, pt4, startT, endT + (endT - startT) * .25, arcStatus) else: # to the bottom! if prevStatus is True: return center, startAngle, endAngle, radius, endT else: return binaryArcApprox(pt1, pt2, pt3, pt4, startT, midT, arcStatus)
def getExtremaForCubic( pt0: Point, pt1: Point, pt2: Point, pt3: Point, h: bool = True, v: bool = False, include_start_end: bool = False, ) -> List[float]: """ Return a list of t values at which the cubic curve defined by pt0, pt1, pt2, pt3 has extrema. :param h: Calculate extrema for horizontal derivative == 0 (= what type designers call vertical extrema!). :type h: bool :param v: Calculate extrema for vertical derivative == 0 (= what type designers call horizontal extrema!). :type v: bool :param include_start_end: Also calculate extrema that lie at the start or end point of the curve. :type include_start_end: bool """ (ax, ay), (bx, by), c, _d = calcCubicParameters(pt0, pt1, pt2, pt3) ax *= 3.0 ay *= 3.0 bx *= 2.0 by *= 2.0 roots = [] if include_start_end: if h: roots = [t for t in solveQuadratic(ay, by, c[1]) if 0 <= t <= 1] if v: roots += [t for t in solveQuadratic(ax, bx, c[0]) if 0 <= t <= 1] else: if h: roots = [t for t in solveQuadratic(ay, by, c[1]) if 0 < t < 1] if v: roots += [t for t in solveQuadratic(ax, bx, c[0]) if 0 < t < 1] return roots
def split_at_extrema(pt1, pt2, pt3, pt4, transform=Transform()): """ Add extrema to a cubic curve, after applying a transformation. Example :: >>> # A segment in which no extrema will be added. >>> split_at_extrema((297, 52), (406, 52), (496, 142), (496, 251)) [((297, 52), (406, 52), (496, 142), (496, 251))] >>> from fontTools.misc.transform import Transform >>> split_at_extrema((297, 52), (406, 52), (496, 142), (496, 251), Transform().rotate(-27)) [((297.0, 52.0), (84.48072108963274, -212.56513799170233), (15.572491694678519, -361.3686192413668), (15.572491694678547, -445.87035970621713)), ((15.572491694678547, -445.8703597062171), (15.572491694678554, -506.84825401175414), (51.4551516055374, -534.3422304091257), (95.14950889754756, -547.6893014808263))] """ # Transform the points for extrema calculation; # transform is expected to rotate the points by - nib angle. t2, t3, t4 = transform.transformPoints([pt2, pt3, pt4]) # When pt1 is the current point of the path, it is already transformed, so # we keep it like it is. t1 = pt1 (ax, ay), (bx, by), c, d = calcCubicParameters(t1, t2, t3, t4) ax *= 3.0 ay *= 3.0 bx *= 2.0 by *= 2.0 # vertical roots = [t for t in solveQuadratic(ay, by, c[1]) if 0 < t < 1] # horizontal roots += [t for t in solveQuadratic(ax, bx, c[0]) if 0 < t < 1] # Use only unique roots and sort them # They should be unique before, or can a root be duplicated (in h and v?) roots = sorted(set(roots)) if not roots: return [(t1, t2, t3, t4)] return splitCubicAtT(t1, t2, t3, t4, *roots)
def eqQuadratic(p0, p1, p2, p3): # Nearest quadratic bezier (TT curve) #print("In: ", p0, p1, p2, p3) a, b, c, d = calcCubicParameters((p0.x, p0.y), (p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y)) #print("Par: %0.0f x^3 + %0.0f x^2 + %0.0f x + %0.0f" % (a[0], b[0], c[0], d[0])) #print(" %0.0f y^3 + %0.0f y^2 + %0.0f y + %0.0f" % (a[1], b[1], c[1], d[1])) a = (0.0, 0.0) q0, q1, q2, q3 = calcCubicPoints((0.0, 0.0), b, c, d) # Find a cubic for a quadratic: #cp1 = (q0[0] + 2.0/3 * (q1[0] - q0[0]), q0[1] + 2.0/3 * (q1[1] - q0[1])) #cp2 = (q2[0] + 2.0/3 * (q1[0] - q2[0]), q2[1] + 2.0/3 * (q1[1] - q2[1])) #print("Out:", q0, q1, q2, q3) scaleX = (p3.x - p0.x) / (q3[0] - q0[0]) scaleY = (p3.y - p0.y) / (q3[1] - q0[1]) #print(scaleX, scaleY) p1.x = (q1[0] - q0[0]) * scaleX + q0[0] p1.y = (q1[1] - q0[1]) * scaleY + q0[1] p2.x = (q2[0] - q0[0]) * scaleX + q0[0] p2.y = (q2[1] - q0[1]) * scaleY + q0[1] p3.x = (q3[0] - q0[0]) * scaleX + q0[0] p3.y = (q3[1] - q0[1]) * scaleY + q0[1] #print(p0, p1, p2, p3) return p1, p2
def eqQuadratic(self, p0, p1, p2, p3): # Nearest quadratic bezier (TT curve) #print "In: ", p0, p1, p2, p3 a, b, c, d = calcCubicParameters((p0.x, p0.y), (p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y)) #print "Par: %0.0f x^3 + %0.0f x^2 + %0.0f x + %0.0f" % (a[0], b[0], c[0], d[0]) #print " %0.0f y^3 + %0.0f y^2 + %0.0f y + %0.0f" % (a[1], b[1], c[1], d[1]) a = (0.0, 0.0) q0, q1, q2, q3 = calcCubicPoints((0.0, 0.0), b, c, d) # Find a cubic for a quadratic: #cp1 = (q0[0] + 2.0/3 * (q1[0] - q0[0]), q0[1] + 2.0/3 * (q1[1] - q0[1])) #cp2 = (q2[0] + 2.0/3 * (q1[0] - q2[0]), q2[1] + 2.0/3 * (q1[1] - q2[1])) #print "Out:", q0, q1, q2, q3 scaleX = (p3.x - p0.x) / (q3[0] - q0[0]) scaleY = (p3.y - p0.y) / (q3[1] - q0[1]) #print scaleX, scaleY p1.x = (q1[0] - q0[0]) * scaleX + q0[0] p1.y = (q1[1] - q0[1]) * scaleY + q0[1] p2.x = (q2[0] - q0[0]) * scaleX + q0[0] p2.y = (q2[1] - q0[1]) * scaleY + q0[1] p3.x = (q3[0] - q0[0]) * scaleX + q0[0] p3.y = (q3[1] - q0[1]) * scaleY + q0[1] #print p0, p1, p2, p3 return p1, p2
def eqQuadratic(self, p0, p1, p2, p3): # Nearest quadratic bezier (TT curve) #print "In: ", p0, p1, p2, p3 a, b, c, d = calcCubicParameters((p0.x, p0.y), (p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y)) #print "Par: %0.0f x^3 + %0.0f x^2 + %0.0f x + %0.0f" % (a[0], b[0], c[0], d[0]) #print " %0.0f y^3 + %0.0f y^2 + %0.0f y + %0.0f" % (a[1], b[1], c[1], d[1]) a = (0.0, 0.0) q0, q1, q2, q3 = calcCubicPoints((0.0, 0.0), b, c, d) # Find a cubic for a quadratic: #cp1 = (q0[0] + 2.0/3 * (q1[0] - q0[0]), q0[1] + 2.0/3 * (q1[1] - q0[1])) #cp2 = (q2[0] + 2.0/3 * (q1[0] - q2[0]), q2[1] + 2.0/3 * (q1[1] - q2[1])) #print "Out:", q0, q1, q2, q3 scaleX = (p3.x - p0.x) / (q3[0] - q0[0]) scaleY = (p3.y - p0.y) / (q3[1] - q0[1]) # THESE LINES ARE ACTUALLY MOVING POINTS p1.x = (q1[0] - q0[0]) * scaleX + q0[0] p1.y = (q1[1] - q0[1]) * scaleY + q0[1] p2.x = (q2[0] - q0[0]) * scaleX + q0[0] p2.y = (q2[1] - q0[1]) * scaleY + q0[1] p3.x = (q3[0] - q0[0]) * scaleX + q0[0] p3.y = (q3[1] - q0[1]) * scaleY + q0[1] #print p0, p1, p2, p3 return p1, p2
def params(self) -> Tuple[Point, Point, Point, Point]: if self._params is None: self._params = calcCubicParameters(self.p0, self.p1, self.p2, self.p3) return self._params
segments = zip(points, points[1:] + [points[0]]) # get the area area = sum([x0 * y1 - x1 * y0 for ((x0, y0), (x1, y1)) in segments]) return area <= 0 # ---------- # Misc. Math # ---------- def _tValueForPointOnCubicCurve(point, (pt1, pt2, pt3, pt4), isHorizontal=0): """ Finds a t value on a curve from a point. The points must be originaly be a point on the curve. This will only back trace the t value, needed to split the curve in parts """ a, b, c, d = bezierTools.calcCubicParameters(pt1, pt2, pt3, pt4) solutions = bezierTools.solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal], d[isHorizontal] - point[isHorizontal]) solutions = [t for t in solutions if 0 <= t < 1] if not solutions and not isHorizontal: # can happen that a horizontal line doens intersect, try the vertical return _tValueForPointOnCubicCurve(point, (pt1, pt2, pt3, pt4), isHorizontal=1) if len(solutions) > 1: intersectionLenghts = {} for t in solutions: tp = _getCubicPoint(t, pt1, pt2, pt3, pt4) dist = _distance(tp, point) intersectionLenghts[dist] = t minDist = min(intersectionLenghts.keys()) solutions = [intersectionLenghts[minDist]] return solutions
area = sum([x0 * y1 - x1 * y0 for ((x0, y0), (x1, y1)) in segments]) return area <= 0 # ---------- # Misc. Math # ---------- def _tValueForPointOnCubicCurve(point, (pt1, pt2, pt3, pt4), isHorizontal=0): """ Finds a t value on a curve from a point. The points must be originaly be a point on the curve. This will only back trace the t value, needed to split the curve in parts """ a, b, c, d = bezierTools.calcCubicParameters(pt1, pt2, pt3, pt4) solutions = bezierTools.solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal], d[isHorizontal] - point[isHorizontal]) solutions = [t for t in solutions if 0 <= t < 1] if not solutions and not isHorizontal: # can happen that a horizontal line doens intersect, try the vertical return _tValueForPointOnCubicCurve(point, (pt1, pt2, pt3, pt4), isHorizontal=1) if len(solutions) > 1: intersectionLenghts = {} for t in solutions: tp = _getCubicPoint(t, pt1, pt2, pt3, pt4) dist = _distance(tp, point) intersectionLenghts[dist] = t minDist = min(intersectionLenghts.keys())