def test_add_curves4_with_gap(): path = Path() c1 = Bezier4P(((0, 0, 0), (0, 1, 0), (2, 1, 0), (2, 0, 0))) c2 = Bezier4P(((2, -1, 0), (2, -2, 0), (0, -2, 0), (0, -1, 0))) tools.add_bezier4p(path, [c1, c2]) assert len(path) == 3 # added a line segment between curves assert path.end == (0, -1, 0)
def test_add_curves(): path = Path() c1 = Bezier4P(((0, 0, 0), (0, 1, 0), (2, 1, 0), (2, 0, 0))) c2 = Bezier4P(((2, 0, 0), (2, -1, 0), (0, -1, 0), (0, 0, 0))) path.add_curves([c1, c2]) assert len(path) == 2 assert path.end == (0, 0, 0)
def test_add_curves4(): path = Path() c1 = Bezier4P(((0, 0), (0, 1), (2, 1), (2, 0))) c2 = Bezier4P(((2, 0), (2, -1), (0, -1), (0, 0))) tools.add_bezier4p(path, [c1, c2]) assert len(path) == 2 assert path.end == (0, 0)
def add_spline(path: Path, spline: BSpline, level=4, reset=True) -> None: """ Add a B-spline as multiple cubic Bèzier-curves. Non-rational B-splines of 3rd degree gets a perfect conversion to cubic bezier curves with a minimal count of curve segments, all other B-spline require much more curve segments for approximation. Auto-detect the connection point to the given `path`, if neither the start- nor the end point of the B-spline is close to the path end point, a line from the path end point to the start point of the B-spline will be added automatically. (see :meth:`add_bezier4p`). By default the start of an **empty** path is set to the start point of the spline, setting argument `reset` to ``False`` prevents this behavior. Args: path: :class:`~ezdxf.path.Path` object spline: B-spline parameters as :class:`~ezdxf.math.BSpline` object level: subdivision level of approximation segments reset: set start point to start of spline if path is empty """ if len(path) == 0 and reset: path.start = spline.point(0) if spline.degree == 3 and not spline.is_rational and spline.is_clamped: curves = [Bezier4P(points) for points in spline.bezier_decomposition()] else: curves = spline.cubic_bezier_approximation(level=level) add_bezier4p(path, curves)
def approximate(self) -> Iterable[Vec3]: control_points = [ self.start, self.start + self.start_tangent, self.end + self.end_tangent, self.end, ] bezier = Bezier4P(control_points) return bezier.approximate(self.segments)
def quadratic_to_cubic_bezier(curve: Bezier3P) -> Bezier4P: """Convert quadratic Bèzier curves (:class:`ezdxf.math.Bezier3P`) into cubic Bèzier curves (:class:`ezdxf.math.Bezier4P`). .. versionadded: 0.16 """ start, control, end = curve.control_points control_1 = start + 2 * (control - start) / 3 control_2 = end + 2 * (control - end) / 3 return Bezier4P((start, control_1, control_2, end))
def cubic_bezier_interpolation( points: Iterable['Vertex']) -> Iterable[Bezier4P]: """ Returns an interpolation curve for given data `points` as multiple cubic Bézier-curves. Returns n-1 cubic Bézier-curves for n given data points, curve i goes from point[i] to point[i+1]. Args: points: data points .. versionadded:: 0.13 """ from ezdxf.math import tridiagonal_matrix_solver # Source: https://towardsdatascience.com/b%C3%A9zier-interpolation-8033e9a262c2 points = Vec3.tuple(points) if len(points) < 3: raise ValueError('At least 3 points required.') num = len(points) - 1 # setup tri-diagonal matrix (a, b, c) b = [4.0] * num a = [1.0] * num c = [1.0] * num b[0] = 2.0 b[num - 1] = 7.0 a[num - 1] = 2.0 # setup right-hand side quantities points_vector = [points[0] + 2.0 * points[1]] points_vector.extend(2.0 * (2.0 * points[i] + points[i + 1]) for i in range(1, num - 1)) points_vector.append(8.0 * points[num - 1] + points[num]) # solve tri-diagonal linear equation system solution = tridiagonal_matrix_solver((a, b, c), points_vector) control_points_1 = Vec3.list(solution.rows()) control_points_2 = [ p * 2.0 - cp for p, cp in zip(points[1:], control_points_1[1:]) ] control_points_2.append((control_points_1[num - 1] + points[num]) / 2.0) for defpoints in zip(points, control_points_1, control_points_2, points[1:]): yield Bezier4P(defpoints)
def approximate(self, segments: int = 20) -> Iterable[Vector]: """ Approximate path by vertices, `segments` is the count of approximation segments for each cubic bezier curve. """ if not self._commands: return start = self._start yield start for cmd in self._commands: type_ = cmd[0] end_location = cmd[1] if type_ == Command.LINE_TO: yield end_location elif type_ == Command.CURVE_TO: pts = iter(Bezier4P((start, cmd[2], cmd[3], end_location)).approximate(segments)) next(pts) # skip first vertex yield from pts else: raise ValueError(f'Invalid command: {type_}') start = end_location
def test_add_curves4_reverse(): path = Path(start=(0, 0, 0)) c1 = Bezier4P(((2, 0, 0), (2, 1, 0), (0, 1, 0), (0, 0, 0))) tools.add_bezier4p(path, [c1]) assert len(path) == 1 assert path.end == (2, 0, 0)
def test_add_curves_reverse(): path = Path(start=(0, 0, 0)) c1 = Bezier4P(((2, 0, 0), (2, 1, 0), (0, 1, 0), (0, 0, 0))) path.add_curves([c1]) assert len(path) == 1 assert path.end == (2, 0, 0)
def random_vec() -> Vec3: return Vec3(r.uniform(-10, 10), r.uniform(-10, 10), r.uniform(-10, 10)) for i in range(1000): quadratic = Bezier3P((random_vec(), random_vec(), random_vec())) quadratic_approx = list(quadratic.approximate(10)) cubic = quadratic_to_cubic_bezier(quadratic) cubic_approx = list(cubic.approximate(10)) assert len(quadratic_approx) == len(cubic_approx) for p1, p2 in zip(quadratic_approx, cubic_approx): assert p1.isclose(p2) # G1 continuity: normalized end-tangent == normalized start-tangent of next curve B1 = Bezier4P([(0, 0), (1, 1), (2, 1), (3, 0)]) # B1/B2 has G1 continuity: B2 = Bezier4P([(3, 0), (4, -1), (5, -1), (6, 0)]) # B1/B3 has no G1 continuity: B3 = Bezier4P([(3, 0), (4, 1), (5, 1), (6, 0)]) # B1/B4 G1 continuity off tolerance: B4 = Bezier4P([(3, 0), (4, -1.03), (5, -1.0), (6, 0)]) # B1/B5 has a gap between B1 end and B5 start: B5 = Bezier4P([(4, 0), (5, -1), (6, -1), (7, 0)]) def test_g1_continuity_for_bezier_curves():
def to_bsplines_and_vertices(path: Path, g1_tol: float = G1_TOL) -> Iterable[PathParts]: """ Convert a :class:`Path` object into multiple cubic B-splines and polylines as lists of vertices. Breaks adjacent Bèzier without G1 continuity into separated B-splines. Args: path: :class:`Path` objects g1_tol: tolerance for G1 continuity check Returns: :class:`~ezdxf.math.BSpline` and lists of :class:`~ezdxf.math.Vec3` .. versionadded:: 0.16 """ from ezdxf.math import bezier_to_bspline def to_vertices(): points = [polyline[0][0]] for line in polyline: points.append(line[1]) return points def to_bspline(): b1 = bezier[0] _g1_continuity_curves = [b1] for b2 in bezier[1:]: if have_bezier_curves_g1_continuity(b1, b2, g1_tol): _g1_continuity_curves.append(b2) else: yield bezier_to_bspline(_g1_continuity_curves) _g1_continuity_curves = [b2] b1 = b2 if _g1_continuity_curves: yield bezier_to_bspline(_g1_continuity_curves) prev = path.start curves = [] for cmd in path: if cmd.type == Command.CURVE3_TO: curve = Bezier3P([prev, cmd.ctrl, cmd.end]) elif cmd.type == Command.CURVE4_TO: curve = Bezier4P([prev, cmd.ctrl1, cmd.ctrl2, cmd.end]) elif cmd.type == Command.LINE_TO: curve = (prev, cmd.end) else: raise ValueError curves.append(curve) prev = cmd.end bezier = [] polyline = [] for curve in curves: if isinstance(curve, tuple): if bezier: yield from to_bspline() bezier.clear() polyline.append(curve) else: if polyline: yield to_vertices() polyline.clear() bezier.append(curve) if bezier: yield from to_bspline() if polyline: yield to_vertices()
def approx_curve4(s, c1, c2, e) -> Iterable[Vec3]: return Bezier4P((s, c1, c2, e)).flattening(distance, segments)
def approx_curve4(s, c1, c2, e) -> Iterable[Vec3]: return Bezier4P((s, c1, c2, e)).approximate(segments)
def quadratic_to_cubic_bezier(curve: Bezier3P) -> Bezier4P: start, control, end = curve.control_points control_1 = start + 2 * (control - start) / 3 control_2 = end + 2 * (control - end) / 3 return Bezier4P((start, control_1, control_2, end))
def approx(self): return ApproxParamT(Bezier4P([(0, 0), (1, 2), (2, 4), (3, 1)]))