def mtext(num): height = 1.0 width = 1.0 p1 = Vector(0, 0, 0) t = MText.new(dxfattribs={ 'char_height': height, 'width': width, 'text_direction': (1, 0, 0), 'attachment_point': 7, 'layer': 'text', }, doc=doc) t.text = content.format(num) tlen = height * len(t.text) * width p2 = p1.replace(x=tlen) p3 = p2.replace(y=height) p4 = p1.replace(y=height) v = [p1, p2, p3, p4, p3.lerp(p4), p2.lerp(p3)] return t, v
def vertex_attribs(data: Sequence[float], format='xyseb') -> dict: """ Create VERTEX attributes from input data. Format codes: - ``x`` = x-coordinate - ``y`` = y-coordinate - ``s`` = start width - ``e`` = end width - ``b`` = bulge value - ``v`` = (x, y [,z]) tuple (z-axis is ignored) Args: data: list or tuple of point components format: format string, default is 'xyseb' Returns: dict with keys: 'location', 'bulge', 'start_width', 'end_width' """ attribs = dict() format = [code for code in format.lower() if code in FORMAT_CODES] location = Vector() for code, value in zip(format, data): if code not in FORMAT_CODES: continue if code == 'v': location = cast('Vertex', value) elif code == 'b': attribs['bulge'] = value elif code == 's': attribs['start_width'] = value elif code == 'e': attribs['end_width'] = value elif code == 'x': location = location.replace(x=value) elif code == 'y': location = location.replace(y=value) attribs['location'] = location return attribs
def text(num): height = 1.0 width = 1.0 p1 = Vector(0, 0, 0) t = Text.new( dxfattribs={ 'text': content.format(num), # should easily show reflexion errors 'height': height, 'width': width, 'rotation': 0, 'layer': 'text', }, doc=doc) t.set_pos(p1, align='LEFT') tlen = height * len(t.dxf.text) * width p2 = p1.replace(x=tlen) p3 = p2.replace(y=height) p4 = p1.replace(y=height) v = [p1, p2, p3, p4, p3.lerp(p4), p2.lerp(p3)] return t, v
class Path(abc.Sequence): def __init__(self, start: 'Vertex' = NULLVEC): self._start = Vector(start) self._commands: List[PathElements] = [] def __len__(self): return len(self._commands) def __getitem__(self, item): return self._commands[item] def __iter__(self): return iter(self._commands) def __copy__(self) -> 'Path': """ Returns a new copy of :class:`Path` with shared immutable data. """ copy = Path(self._start) # immutable data copy._commands = list(self._commands) return copy clone = __copy__ @property def start(self) -> Vector: """ :class:`Path` start point, resetting the start point of an empty path is possible. """ return self._start @start.setter def start(self, location: 'Vertex') -> None: if len(self._commands): raise ValueError('Requires an empty path.') else: self._start = Vector(location) @property def end(self) -> Vector: """ :class:`Path` end point. """ if self._commands: return self._commands[-1].end else: return self._start @property def is_closed(self) -> bool: """ Returns ``True`` if the start point is close to the end point. """ return self._start.isclose(self.end) @classmethod def from_vertices(cls, vertices: Iterable['Vertex'], close=False) -> 'Path': """ Returns a :class:`Path` from vertices. """ vertices = Vector.list(vertices) if len(vertices) < 2: return cls() path = cls(start=vertices[0]) for vertex in vertices[1:]: path.line_to(vertex) if close: path.close() return path @classmethod def from_lwpolyline(cls, lwpolyline: 'LWPolyline') -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.LWPolyline` entity, all vertices transformed to WCS. """ assert lwpolyline.dxftype() == 'LWPOLYLINE' path = cls() path._setup_polyline_2d( lwpolyline.get_points('xyb'), close=lwpolyline.closed, ocs=lwpolyline.ocs(), elevation=lwpolyline.dxf.elevation, ) return path @classmethod def from_polyline(cls, polyline: 'Polyline') -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Polyline` entity, all vertices transformed to WCS. """ assert polyline.dxftype() == 'POLYLINE' path = cls() if len(polyline.vertices) == 0: return path if polyline.is_3d_polyline: return cls.from_vertices(polyline.points(), polyline.is_closed) points = [vertex.format('xyb') for vertex in polyline.vertices] ocs = polyline.ocs() if polyline.dxf.hasattr('elevation'): elevation = Vector(polyline.dxf.elevation).z else: # Elevation attribute is mandatory, but you never know, # take elevation from first vertex. elevation = Vector(polyline.vertices[0].dxf.location).z path._setup_polyline_2d( points, close=polyline.is_closed, ocs=ocs, elevation=elevation, ) return path def _setup_polyline_2d(self, points: Iterable[Sequence[float]], close: bool, ocs: OCS, elevation: float) -> None: def bulge_to(p1: Vector, p2: Vector, bulge: float): if p1.isclose(p2): return center, start_angle, end_angle, radius = bulge_to_arc( p1, p2, bulge) ellipse = ConstructionEllipse.from_arc( center, radius, Z_AXIS, math.degrees(start_angle), math.degrees(end_angle), ) curves = list(cubic_bezier_from_ellipse(ellipse)) if curves[0].control_points[0].isclose(p2): curves = _reverse_bezier_curves(curves) self.add_curves(curves) prev_point = None prev_bulge = 0 for x, y, bulge in points: point = Vector(x, y) if prev_point is None: self._start = point prev_point = point prev_bulge = bulge continue if prev_bulge: bulge_to(prev_point, point, prev_bulge) else: self.line_to(point) prev_point = point prev_bulge = bulge if close and not self.start.isclose(self.end): if prev_bulge: bulge_to(self.end, self.start, prev_bulge) else: self.line_to(self.start) if ocs.transform or elevation: self._to_wcs(ocs, elevation) def _to_wcs(self, ocs: OCS, elevation: float): self._start = ocs.to_wcs(self._start.replace(z=elevation)) for i, cmd in enumerate(self._commands): self._commands[i] = cmd.to_wcs(ocs, elevation) @classmethod def from_spline(cls, spline: 'Spline', level: int = 4) -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Spline`. """ path = cls() path.add_spline(spline.construction_tool(), level=level, reset=True) return path @classmethod def from_ellipse(cls, ellipse: 'Ellipse', segments: int = 1) -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Ellipse`. """ path = cls() path.add_ellipse(ellipse.construction_tool(), segments=segments, reset=True) return path @classmethod def from_arc(cls, arc: 'Arc', segments: int = 1) -> 'Path': """ Returns a :class:`Path` from an :class:`~ezdxf.entities.Arc`. """ path = cls() radius = abs(arc.dxf.radius) if not math.isclose(radius, 0): ellipse = ConstructionEllipse.from_arc( center=arc.dxf.center, radius=radius, extrusion=arc.dxf.extrusion, start_angle=arc.dxf.start_angle, end_angle=arc.dxf.end_angle, ) path.add_ellipse(ellipse, segments=segments, reset=True) return path @classmethod def from_circle(cls, circle: 'Circle', segments: int = 1) -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Circle`. """ path = cls() radius = abs(circle.dxf.radius) if not math.isclose(radius, 0): ellipse = ConstructionEllipse.from_arc( center=circle.dxf.center, radius=radius, extrusion=circle.dxf.extrusion, ) path.add_ellipse(ellipse, segments=segments, reset=True) return path @classmethod def from_hatch_polyline_path(cls, polyline: 'PolylinePath', ocs: OCS = None, elevation: float = 0) -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Hatch` polyline path. """ path = cls() path._setup_polyline_2d( polyline.vertices, # List[(x, y, bulge)] close=polyline.is_closed, ocs=ocs or OCS(), elevation=elevation, ) return path @classmethod def from_hatch_edge_path(cls, edges: 'EdgePath', ocs: OCS = None, elevation: float = 0) -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Hatch` edge path. """ def add_line_edge(edge): start = wcs(edge.start) end = wcs(edge.end) if len(path): if path.end.isclose(start): # path-end -> line-end path.line_to(end) elif path.end.isclose(end): # path-end (==line-end) -> line-start path.line_to(start) else: # path-end -> edge-start -> edge-end path.line_to(start) path.line_to(end) else: # start path path.start = start path.line_to(end) def add_arc_edge(edge): x, y, *_ = edge.center # from_arc() requires OCS data: ellipse = ConstructionEllipse.from_arc( center=(x, y, elevation), radius=edge.radius, extrusion=extrusion, start_angle=edge.start_angle, end_angle=edge.end_angle, ) path.add_ellipse(ellipse, reset=not bool(path)) def add_ellipse_edge(edge): ocs_ellipse = edge.construction_tool() # ConstructionEllipse has WCS representation: ellipse = ConstructionEllipse( center=wcs(ocs_ellipse.center.replace(z=elevation)), major_axis=wcs(ocs_ellipse.major_axis), ratio=ocs_ellipse.ratio, extrusion=extrusion, start_param=ocs_ellipse.start_param, end_param=ocs_ellipse.end_param, ) path.add_ellipse(ellipse, reset=not bool(path)) def add_spline_edge(edge): control_points = [wcs(p) for p in edge.control_points] if len(control_points) == 0: fit_points = [wcs(p) for p in edge.fit_points] if len(fit_points): bspline = from_fit_points(edge, fit_points) else: # No control points and no fit points: # DXF structure error return else: bspline = from_control_points(edge, control_points) path.add_spline(bspline, reset=not bool(path)) def from_fit_points(edge, fit_points): tangents = None if edge.start_tangent and edge.end_tangent: tangents = (wcs(edge.start_tangent), wcs(edge.end_tangent)) return global_bspline_interpolation( fit_points, degree=edge.degree, tangents=tangents, ) def from_control_points(edge, control_points): return BSpline(control_points=control_points, order=edge.degree + 1, knots=edge.knot_values, weights=edge.weights if edge.weights else None) def wcs(vertex): if ocs and ocs.transform: return ocs.to_wcs((vertex.x, vertex.y, elevation)) else: return Vector(vertex) extrusion = ocs.uz if ocs else Z_AXIS path = Path() for edge in edges: if edge.EDGE_TYPE == "LineEdge": add_line_edge(edge) elif edge.EDGE_TYPE == "ArcEdge": if not math.isclose(edge.radius, 0): add_arc_edge(edge) elif edge.EDGE_TYPE == "EllipseEdge": if not NULLVEC.isclose(edge.major_axis): add_ellipse_edge(edge) elif edge.EDGE_TYPE == "SplineEdge": add_spline_edge(edge) return path def control_vertices(self): """ Yields all path control vertices in consecutive order. """ if len(self): yield self.start for cmd in self._commands: if cmd.type == Command.LINE_TO: yield cmd.end elif cmd.type == Command.CURVE_TO: yield cmd.ctrl1 yield cmd.ctrl2 yield cmd.end def has_clockwise_orientation(self) -> bool: """ Returns ``True`` if 2D path has clockwise orientation, ignores z-axis of all control vertices. """ return has_clockwise_orientation(self.control_vertices()) def line_to(self, location: 'Vertex') -> None: """ Add a line from actual path end point to `location`. """ self._commands.append(LineTo(end=Vector(location))) def curve_to(self, location: 'Vertex', ctrl1: 'Vertex', ctrl2: 'Vertex') -> None: """ Add a cubic Bèzier-curve from actual path end point to `location`, `ctrl1` and `ctrl2` are the control points for the cubic Bèzier-curve. """ self._commands.append( CurveTo(end=Vector(location), ctrl1=Vector(ctrl1), ctrl2=Vector(ctrl2))) def close(self) -> None: """ Close path by adding a line segment from the end point to the start point. """ if not self.is_closed: self.line_to(self.start) def reversed(self) -> 'Path': """ Returns a new :class:`Path` with reversed segments and control vertices. """ if len(self) == 0: return Path() path = Path(start=self.end) for index in range(len(self) - 1, -1, -1): cmd = self[index] if index > 0: prev_end = self[index - 1].end else: prev_end = self.start if cmd.type == Command.LINE_TO: path.line_to(prev_end) elif cmd.type == Command.CURVE_TO: path.curve_to(prev_end, cmd.ctrl2, cmd.ctrl1) return path def clockwise(self) -> 'Path': """ Returns new :class:`Path` in clockwise orientation. """ if self.has_clockwise_orientation(): return self.clone() else: return self.reversed() def counter_clockwise(self) -> 'Path': """ Returns new :class:`Path` in counter-clockwise orientation. """ if self.has_clockwise_orientation(): return self.reversed() else: return self.clone() def add_curves(self, curves: Iterable[Bezier4P]) -> None: """ Add multiple cubic Bèzier-curves to the path. Auto-detect if the path end point is connected to the start- or end point of the curves, if none of them is close to the path end point a line from the path end point to the curves start point will be added. """ curves = list(curves) if not len(curves): return end = curves[-1].control_points[-1] if self.end.isclose(end): # connect to new curves end point curves = _reverse_bezier_curves(curves) for curve in curves: start, ctrl1, ctrl2, end = curve.control_points if not start.isclose(self.end, abs_tol=1e-9): self.line_to(start) self.curve_to(end, ctrl1, ctrl2) def add_ellipse(self, ellipse: ConstructionEllipse, segments=1, reset=True) -> None: """ Add an elliptical arc as multiple cubic Bèzier-curves, use :meth:`~ezdxf.math.ConstructionEllipse.from_arc` constructor of class :class:`~ezdxf.math.ConstructionEllipse` to add circular arcs. Auto-detect connection point, if none is close a line from the path end point to the ellipse start point will be added (see :meth:`add_curves`). By default the start of an **empty** path is set to the start point of the ellipse, setting argument `reset` to ``False`` prevents this behavior. Args: ellipse: ellipse parameters as :class:`~ezdxf.math.ConstructionEllipse` object segments: count of Bèzier-curve segments, at least one segment for each quarter (pi/2), ``1`` for as few as possible. reset: set start point to start of ellipse if path is empty """ if len(self) == 0 and reset: self.start = ellipse.start_point self.add_curves(cubic_bezier_from_ellipse(ellipse, segments)) def add_spline(self, 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 connection point, if none is close a line from the path end point to the spline start point will be added (see :meth:`add_curves`). 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: 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(self) == 0 and reset: self.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) self.add_curves(curves) 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: end_location = cmd.end if cmd.type == Command.LINE_TO: yield end_location elif cmd.type == Command.CURVE_TO: pts = iter( Bezier4P((start, cmd.ctrl1, cmd.ctrl2, end_location)).approximate(segments)) next(pts) # skip first vertex yield from pts else: raise ValueError(f'Invalid command: {cmd.type}') start = end_location def transform(self, m: 'Matrix44') -> 'Path': """ Returns a new transformed path. Args: m: transformation matrix of type :class:`~ezdxf.math.Matrix44` """ new_path = self.__class__(m.transform(self.start)) for cmd in self._commands: if cmd.type == Command.LINE_TO: new_path.line_to(m.transform(cmd.end)) elif cmd.type == Command.CURVE_TO: loc, ctrl1, ctrl2 = m.transform_vertices( (cmd.end, cmd.ctrl1, cmd.ctrl2)) new_path.curve_to(loc, ctrl1, ctrl2) else: raise ValueError(f'Invalid command: {cmd.type}') return new_path
class Path(abc.Sequence): def __init__(self, start: 'Vertex' = NULLVEC): self._start = Vector(start) self._commands: List[Tuple] = [] def __len__(self): return len(self._commands) def __getitem__(self, item): return self._commands[item] def __iter__(self): return iter(self._commands) @property def start(self) -> Vector: """ :class:`Path` start point, resetting the start point of an empty path is possible. """ return self._start @start.setter def start(self, location: 'Vertex') -> None: if len(self._commands): raise ValueError('Requires an empty path.') else: self._start = Vector(location) @property def end(self) -> Vector: """ :class:`Path` end point. """ if self._commands: return self._commands[-1][1] else: return self._start @property def is_closed(self) -> bool: """ Returns ``True`` if the start point is close to the end point. """ return self._start.isclose(self.end) @classmethod def from_vertices(cls, vertices: Iterable['Vertex'], close=False) -> 'Path': """ Returns a :class:`Path` from vertices. """ vertices = Vector.list(vertices) path = cls(start=vertices[0]) for vertex in vertices[1:]: path.line_to(vertex) if close: path.close() return path @classmethod def from_lwpolyline(cls, lwpolyline: 'LWPolyline') -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.LWPolyline` entity, all vertices transformed to WCS. """ assert lwpolyline.dxftype() == 'LWPOLYLINE' path = cls() path._setup_polyline_2d( lwpolyline.get_points('xyb'), close=lwpolyline.closed, ocs=lwpolyline.ocs(), elevation=lwpolyline.dxf.elevation, ) return path @classmethod def from_polyline(cls, polyline: 'Polyline') -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Polyline` entity, all vertices transformed to WCS. """ assert polyline.dxftype() == 'POLYLINE' path = cls() if len(polyline.vertices) == 0: return path if polyline.is_3d_polyline: return cls.from_vertices(polyline.points(), polyline.is_closed) points = [vertex.format('xyb') for vertex in polyline.vertices] ocs = polyline.ocs() if polyline.dxf.hasattr('elevation'): elevation = Vector(polyline.dxf.elevation).z else: # Elevation attribute is mandatory, but you never know, # take elevation from first vertex. elevation = Vector(polyline.vertices[0].dxf.location).z path._setup_polyline_2d( points, close=polyline.is_closed, ocs=ocs, elevation=elevation, ) return path def _setup_polyline_2d(self, points: Iterable[Sequence[float]], close: bool, ocs: OCS, elevation: float) -> None: def bulge_to(p1: Vector, p2: Vector, bulge: float): if p1.isclose(p2): return center, start_angle, end_angle, radius = bulge_to_arc(p1, p2, bulge) ellipse = ConstructionEllipse.from_arc( center, radius, Z_AXIS, math.degrees(start_angle), math.degrees(end_angle), ) curves = list(cubic_bezier_from_ellipse(ellipse)) if curves[0].control_points[0].isclose(p2): curves = _reverse_bezier_curves(curves) self.add_curves(curves) prev_point = None prev_bulge = 0 for x, y, bulge in points: point = Vector(x, y) if prev_point is None: self._start = point prev_point = point prev_bulge = bulge continue if prev_bulge: bulge_to(prev_point, point, prev_bulge) else: self.line_to(point) prev_point = point prev_bulge = bulge if close and not self.start.isclose(self.end): if prev_bulge: bulge_to(self.end, self.start, prev_bulge) else: self.line_to(self.start) if ocs.transform or elevation: self._to_wcs(ocs, elevation) def _to_wcs(self, ocs: OCS, elevation: float): self._start = ocs.to_wcs(self._start.replace(z=elevation)) for i, cmd in enumerate(self._commands): new_cmd = [cmd[0]] new_cmd.extend(ocs.points_to_wcs(p.replace(z=elevation) for p in cmd[1:])) self._commands[i] = tuple(new_cmd) @classmethod def from_spline(cls, spline: 'Spline', level: int = 4) -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Spline`. """ path = cls() path.add_spline(spline.construction_tool(), level=level, reset=True) return path @classmethod def from_ellipse(cls, ellipse: 'Ellipse', segments: int = 1) -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Ellipse`. """ path = cls() path.add_ellipse(ellipse.construction_tool(), segments=segments, reset=True) return path @classmethod def from_arc(cls, arc: 'Arc', segments: int = 1) -> 'Path': """ Returns a :class:`Path` from an :class:`~ezdxf.entities.Arc`. """ path = cls() ellipse = ConstructionEllipse.from_arc( center=arc.dxf.center, radius=arc.dxf.radius, extrusion=arc.dxf.extrusion, start_angle=arc.dxf.start_angle, end_angle=arc.dxf.end_angle, ) path.add_ellipse(ellipse, segments=segments, reset=True) return path @classmethod def from_circle(cls, circle: 'Circle', segments: int = 1) -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Circle`. """ path = cls() ellipse = ConstructionEllipse.from_arc( center=circle.dxf.center, radius=circle.dxf.radius, extrusion=circle.dxf.extrusion, ) path.add_ellipse(ellipse, segments=segments, reset=True) return path @classmethod def from_hatch_polyline_path(cls, path: 'PolylinePath') -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Hatch` polyline path. """ pass @classmethod def from_hatch_edge_path(cls, path: 'EdgePath') -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Hatch` edge path. """ pass def line_to(self, location: 'Vertex') -> None: """ Add a line from actual path end point to `location`. """ self._commands.append((Command.LINE_TO, Vector(location))) def curve_to(self, location: 'Vertex', ctrl1: 'Vertex', ctrl2: 'Vertex') -> None: """ Add a cubic Bèzier-curve from actual path end point to `location`, `ctrl1` and `ctrl2` are the control points for the cubic Bèzier-curve. """ self._commands.append((Command.CURVE_TO, Vector(location), Vector(ctrl1), Vector(ctrl2))) def close(self) -> None: """ Close path by adding a line segment from the end point to the start point. """ if not self.is_closed: self.line_to(self.start) def add_curves(self, curves: Iterable[Bezier4P]) -> None: """ Add multiple cubic Bèzier-curves to the path. Auto-detect if the path end point is connected to the start- or end point of the curves, if none of them is close to the path end point a line from the path end point to the curves start point will be added. """ curves = list(curves) if not len(curves): return end = curves[-1].control_points[-1] if self.end.isclose(end): # connect to new curves end point curves = _reverse_bezier_curves(curves) for curve in curves: start, ctrl1, ctrl2, end = curve.control_points if not start.isclose(self.end, abs_tol=1e-9): self.line_to(start) self.curve_to(end, ctrl1, ctrl2) def add_ellipse(self, ellipse: ConstructionEllipse, segments=1, reset=True) -> None: """ Add an elliptical arc as multiple cubic Bèzier-curves, use :meth:`~ezdxf.math.ConstructionEllipse.from_arc` constructor of class :class:`~ezdxf.math.ConstructionEllipse` to add circular arcs. Auto-detect connection point, if none is close a line from the path end point to the ellipse start point will be added (see :meth:`add_curves`). By default the start of an **empty** path is set to the start point of the ellipse, setting argument `reset` to ``False`` prevents this behavior. Args: ellipse: ellipse parameters as :class:`~ezdxf.math.ConstructionEllipse` object segments: count of Bèzier-curve segments, at least one segment for each quarter (pi/2), ``1`` for as few as possible. reset: set start point to start of ellipse if path is empty """ if len(self) == 0 and reset: self.start = ellipse.start_point self.add_curves(cubic_bezier_from_ellipse(ellipse, segments)) def add_spline(self, 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 connection point, if none is close a line from the path end point to the spline start point will be added (see :meth:`add_curves`). 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: 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(self) == 0 and reset: self.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) self.add_curves(curves) 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 transform(self, m: 'Matrix44') -> 'Path': """ Returns a new transformed path. Args: m: transformation matrix of type :class:`~ezdxf.math.Matrix44` """ new_path = self.__class__(m.transform(self.start)) for cmd in self._commands: type_ = cmd[0] if type_ == Command.LINE_TO: new_path.line_to(m.transform(cmd[1])) elif type_ == Command.CURVE_TO: loc, ctrl1, ctrl2 = m.transform_vertices(cmd[1:]) new_path.curve_to(loc, ctrl1, ctrl2) else: raise ValueError(f'Invalid command: {type_}') return new_path