def split_at_pt_fast(self, pt: Point) -> Tuple[Point, Point, Point, Point]: if DEBUG_SPLIT: print("SuperCubic.split_at_pt_fast", pt, "->") index = 0 x, y = pt a, b, c, d = self.cubics[0].params solutions_h = solveCubic(a[1], b[1], c[1], d[1] - y) solutions_v = solveCubic(a[0], b[0], c[0], d[0] - x) solutions_h = [t for t in solutions_h if 0 <= t < 1] solutions_v = [t for t in solutions_v if 0 <= t < 1] if DEBUG_SPLIT: print(solutions_h, solutions_v) if len(solutions_h) == 1 and solutions_v: # Take the average of both values t = (solutions_v[0] + solutions_h[0]) * 0.5 else: print( " Different number of solutions for h and v:", solutions_h, solutions_v, ) index_t = self.t_for_point(pt) if index_t is None: raise ValueError index, t = index_t print(" Choosing via thorough method:", t) self._split_index = index return self.cubics[index].split_at_t(t)
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 _curveToOne(self, bcp1, bcp2, point): x, y = self.testPoint x1, y1 = self._getCurrentPoint() x2, y2 = bcp1 x3, y3 = bcp2 x4, y4 = point if x1 < x and x2 < x and x3 < x and x4 < x: return if y1 < y and y2 < y and y3 < y and y4 < y: return if y1 >= y and y2 >= y and y3 >= y and y4 >= y: return dy = y1 cy = (y2 - dy) * 3.0 by = (y3 - y2) * 3.0 - cy ay = y4 - dy - cy - by solutions = sorted(solveCubic(ay, by, cy, dy - y)) solutions = [t for t in solutions if -0. <= t <= 1.] if not solutions: return dx = x1 cx = (x2 - dx) * 3.0 bx = (x3 - x2) * 3.0 - cx ax = x4 - dx - cx - bx above = y1 >= y lastT = None for t in solutions: if t == lastT: continue lastT = t t2 = t * t t3 = t2 * t direction = 3*ay*t2 + 2*by*t + cy incomingGoingUp = outgoingGoingUp = direction > 0.0 if direction == 0.0: direction = 6*ay*t + 2*by outgoingGoingUp = direction > 0.0 incomingGoingUp = not outgoingGoingUp if direction == 0.0: direction = ay incomingGoingUp = outgoingGoingUp = direction > 0.0 xt = ax*t3 + bx*t2 + cx*t + dx if xt < x: continue if t in (0.0, -0.0): if not outgoingGoingUp: self._addIntersection(outgoingGoingUp) elif t == 1.0: if incomingGoingUp: self._addIntersection(incomingGoingUp) else: if incomingGoingUp == outgoingGoingUp: self._addIntersection(outgoingGoingUp)
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 test_solveCubic(): assert solveCubic(1, 1, -6, 0) == [-3.0, -0.0, 2.0] assert solveCubic(-10.0, -9.0, 48.0, -29.0) == [-2.9, 1.0, 1.0] assert solveCubic(-9.875, -9.0, 47.625, -28.75) == [-2.911392, 1.0, 1.0] assert solveCubic(1.0, -4.5, 6.75, -3.375) == [1.5, 1.5, 1.5] assert solveCubic(-12.0, 18.0, -9.0, 1.50023651123) == [0.5, 0.5, 0.5] assert solveCubic(9.0, 0.0, 0.0, -7.62939453125e-05) == [-0.0, -0.0, -0.0]
def distanceToQuadratic(m, quad): p0, p1, p2 = quad # From http://blog.gludion.com/2009/08/distance-to-quadratic-bezier-curve.html A = p1 - p0 B = p2 - p1 - A # coeffs of third degree polynomial a = B.squaredLength() b = 3.0 * (A | B) mp = p0 - m c = 2.0 * A.squaredLength() + (mp | B) d = (mp | A) pts = [splitQuadratic(t, quad)[0][2] for t in rbt.solveCubic(a,b,c,d) if (t>=eps) and (t<=1.0-eps)] dists = [(m-p).length() for p in (pts+[p0,p2])] return min(dists)
def distanceToQuadratic(m, quad): p0, p1, p2 = quad # From http://blog.gludion.com/2009/08/distance-to-quadratic-bezier-curve.html A = p1 - p0 B = p2 - p1 - A # coeffs of third degree polynomial a = B.squaredLength() b = 3.0 * (A | B) mp = p0 - m c = 2.0 * A.squaredLength() + (mp | B) d = (mp | A) pts = [ splitQuadratic(t, quad)[0][2] for t in rbt.solveCubic(a, b, c, d) if (t >= eps) and (t <= 1.0 - eps) ] dists = [(m - p).length() for p in (pts + [p0, p2])] return min(dists)
def _curveToOne(self, bcp1, bcp2, point): x, y = self.testPoint x1, y1 = self._getCurrentPoint() x2, y2 = bcp1 x3, y3 = bcp2 x4, y4 = point if x1 < x and x2 < x and x3 < x and x4 < x: return if y1 < y and y2 < y and y3 < y and y4 < y: return if y1 >= y and y2 >= y and y3 >= y and y4 >= y: return dy = y1 cy = (y2 - dy) * 3.0 by = (y3 - y2) * 3.0 - cy ay = y4 - dy - cy - by solutions = sorted(solveCubic(ay, by, cy, dy - y)) solutions = [t for t in solutions if ZERO_MINUS_EPSILON <= t <= ONE_PLUS_EPSILON] if not solutions: return dx = x1 cx = (x2 - dx) * 3.0 bx = (x3 - x2) * 3.0 - cx ax = x4 - dx - cx - bx above = y1 >= y lastT = None for t in solutions: if t == lastT: continue lastT = t t2 = t * t t3 = t2 * t direction = 3*ay*t2 + 2*by*t + cy if direction == 0.0: direction = 6*ay*t + 2*by if direction == 0.0: direction = ay goingUp = direction > 0.0 xt = ax*t3 + bx*t2 + cx*t + dx if xt < x: above = goingUp continue if t == 0.0: if not goingUp: self._addIntersection(goingUp) elif t == 1.0: if not above: self._addIntersection(goingUp) else: if above != goingUp: self._addIntersection(goingUp) #else: # we're not really intersecting, merely touching the 'top' above = goingUp
# ---------- # 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
def _curveToOne(self, bcp1, bcp2, point): x, y = self.testPoint x1, y1 = self._getCurrentPoint() x2, y2 = bcp1 x3, y3 = bcp2 x4, y4 = point if x1 < x and x2 < x and x3 < x and x4 < x: return if y1 < y and y2 < y and y3 < y and y4 < y: return if y1 >= y and y2 >= y and y3 >= y and y4 >= y: return dy = y1 cy = (y2 - dy) * 3.0 by = (y3 - y2) * 3.0 - cy ay = y4 - dy - cy - by solutions = sorted(solveCubic(ay, by, cy, dy - y)) solutions = [ t for t in solutions if ZERO_MINUS_EPSILON <= t <= ONE_PLUS_EPSILON ] if not solutions: return dx = x1 cx = (x2 - dx) * 3.0 bx = (x3 - x2) * 3.0 - cx ax = x4 - dx - cx - bx above = y1 >= y lastT = None for t in solutions: if t == lastT: continue lastT = t t2 = t * t t3 = t2 * t direction = 3 * ay * t2 + 2 * by * t + cy if direction == 0.0: direction = 6 * ay * t + 2 * by if direction == 0.0: direction = ay goingUp = direction > 0.0 xt = ax * t3 + bx * t2 + cx * t + dx if xt < x: above = goingUp continue if t == 0.0: if not goingUp: self._addIntersection(goingUp) elif t == 1.0: if not above: self._addIntersection(goingUp) else: if above != goingUp: self._addIntersection(goingUp) #else: # we're not really intersecting, merely touching the 'top' above = goingUp
# 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 def _tValueForPointOnQuadCurve(point, pts, isHorizontal=0):