Files
stepanalyser/.venv/lib/python3.12/site-packages/ezdxf/path/converter.py
Christian Anetzberger a197de9456 initial
2026-01-22 20:23:51 +01:00

961 lines
30 KiB
Python

# Copyright (c) 2020-2023, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
List,
Iterable,
Iterator,
Union,
Optional,
Callable,
Type,
TypeVar,
)
from typing_extensions import TypeAlias
from functools import singledispatch, partial
import logging
from ezdxf.math import (
ABS_TOL,
Vec2,
Vec3,
NULLVEC,
Z_AXIS,
OCS,
Bezier3P,
Bezier4P,
ConstructionEllipse,
BSpline,
have_bezier_curves_g1_continuity,
fit_points_to_cad_cv,
UVec,
Matrix44,
)
from ezdxf.lldxf import const
from ezdxf.entities import (
LWPolyline,
Polyline,
Hatch,
Line,
Spline,
Ellipse,
Arc,
Circle,
Solid,
Trace,
Face3d,
Viewport,
Image,
Helix,
Wipeout,
MPolygon,
BoundaryPaths,
AbstractBoundaryPath,
PolylinePath,
EdgePath,
LineEdge,
ArcEdge,
EllipseEdge,
SplineEdge,
)
from ezdxf.entities.polygon import DXFPolygon
from .path import Path
from .commands import Command
from . import tools
from .nesting import group_paths
__all__ = [
"make_path",
"to_lines",
"to_polylines3d",
"to_lwpolylines",
"to_polylines2d",
"to_hatches",
"to_mpolygons",
"to_bsplines_and_vertices",
"to_splines_and_polylines",
"from_hatch",
"from_hatch_ocs",
"from_hatch_boundary_path",
"from_hatch_edge_path",
"from_hatch_polyline_path",
"from_vertices",
]
MAX_DISTANCE = 0.01
MIN_SEGMENTS = 4
G1_TOL = 1e-4
TPolygon = TypeVar("TPolygon", Hatch, MPolygon)
BoundaryFactory = Callable[[BoundaryPaths, Path, int], None]
logger = logging.getLogger("ezdxf")
@singledispatch
def make_path(entity, segments: int = 1, level: int = 4) -> Path:
"""Factory function to create a single :class:`Path` object from a DXF
entity.
Args:
entity: DXF entity
segments: minimal count of cubic Bézier-curves for elliptical arcs
level: subdivide level for SPLINE approximation
Raises:
TypeError: for unsupported DXF types
"""
# Complete documentation is path.rst, because Sphinx auto-function
# renders for each overloaded function a signature, which is ugly
# and wrong signatures for multiple overloaded function
# e.g. 3 equal signatures for type Solid.
raise TypeError(f"unsupported DXF type: {entity.dxftype()}")
@make_path.register(LWPolyline)
def _from_lwpolyline(lwpolyline: LWPolyline, **kwargs) -> Path:
path = Path()
tools.add_2d_polyline(
path,
lwpolyline.get_points("xyb"),
close=lwpolyline.closed,
ocs=lwpolyline.ocs(),
elevation=lwpolyline.dxf.elevation,
segments=kwargs.get("segments", 1),
)
return path
@make_path.register(Polyline)
def _from_polyline(polyline: Polyline, **kwargs) -> Path:
if polyline.is_polygon_mesh or polyline.is_poly_face_mesh:
raise TypeError("Unsupported DXF type PolyMesh or PolyFaceMesh")
path = Path()
if len(polyline.vertices) == 0:
return path
if polyline.is_3d_polyline:
return 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 = Vec3(polyline.dxf.elevation).z
else:
# the elevation attribute is mandatory, but if it's missing
# take the elevation value of the first vertex.
elevation = Vec3(polyline.vertices[0].dxf.location).z
tools.add_2d_polyline(
path,
points,
close=polyline.is_closed,
ocs=ocs,
elevation=elevation,
segments=kwargs.get("segments", 1),
)
return path
@make_path.register(Helix)
@make_path.register(Spline)
def _from_spline(spline: Spline, **kwargs) -> Path:
level = kwargs.get("level", 4)
path = Path()
tools.add_spline(path, spline.construction_tool(), level=level, reset=True)
return path
@make_path.register(Ellipse)
def _from_ellipse(ellipse: Ellipse, **kwargs) -> Path:
segments = kwargs.get("segments", 1)
path = Path()
tools.add_ellipse(path, ellipse.construction_tool(), segments=segments, reset=True)
return path
@make_path.register(Line)
def _from_line(line: Line, **kwargs) -> Path:
path = Path(line.dxf.start)
path.line_to(line.dxf.end)
return path
@make_path.register(Arc)
def _from_arc(arc: Arc, **kwargs) -> Path:
segments = kwargs.get("segments", 1)
path = Path()
radius = abs(arc.dxf.radius)
if radius > 1e-12:
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,
)
tools.add_ellipse(path, ellipse, segments=segments, reset=True)
return path
@make_path.register(Circle)
def _from_circle(circle: Circle, **kwargs) -> Path:
segments = kwargs.get("segments", 1)
path = Path()
radius = abs(circle.dxf.radius)
if radius > 1e-12:
ellipse = ConstructionEllipse.from_arc(
center=circle.dxf.center,
radius=radius,
extrusion=circle.dxf.extrusion,
)
tools.add_ellipse(path, ellipse, segments=segments, reset=True)
return path
@make_path.register(Face3d)
@make_path.register(Trace)
@make_path.register(Solid)
def _from_quadrilateral(solid: "Solid", **kwargs) -> Path:
vertices = solid.wcs_vertices()
return from_vertices(vertices, close=True)
@make_path.register(Viewport)
def _from_viewport(vp: "Viewport", **kwargs) -> Path:
if vp.has_extended_clipping_path:
handle = vp.dxf.clipping_boundary_handle
if handle != "0" and vp.doc: # exist
db = vp.doc.entitydb
if db: # exist
# Many DXF entities can define a clipping path:
clipping_entity = vp.doc.entitydb.get(handle)
if clipping_entity: # exist
return make_path(clipping_entity, **kwargs)
# Return bounding box:
return from_vertices(vp.clipping_rect_corners(), close=True)
@make_path.register(Wipeout)
@make_path.register(Image)
def _from_image(image: "Image", **kwargs) -> Path:
return from_vertices(image.boundary_path_wcs(), close=True)
@make_path.register(MPolygon)
@make_path.register(Hatch)
def _from_hatch(hatch: Hatch, **kwargs) -> Path:
ocs = hatch.ocs()
elevation = hatch.dxf.elevation.z
offset = NULLVEC
if isinstance(hatch, MPolygon):
offset = hatch.dxf.get("offset_vector", NULLVEC)
try:
paths = [
from_hatch_boundary_path(boundary, ocs, elevation, offset=offset)
for boundary in hatch.paths
]
except const.DXFStructureError:
# TODO: fix problems beforehand in audit process? see issue #1081
logger.warning(f"invalid data in {str(hatch)}")
return Path()
# looses the boundary path state:
return tools.to_multi_path(paths)
def from_hatch(hatch: DXFPolygon, offset: Vec3 = NULLVEC) -> Iterator[Path]:
"""Yield all HATCH/MPOLYGON boundary paths as separated :class:`Path` objects in WCS
coordinates.
"""
ocs = hatch.ocs()
elevation = hatch.dxf.elevation.z
for boundary in hatch.paths:
p = from_hatch_boundary_path(boundary, ocs, elevation=elevation, offset=offset)
if p.has_sub_paths:
yield from p.sub_paths()
else:
yield p
def from_hatch_ocs(hatch: DXFPolygon, offset: Vec3 = NULLVEC) -> Iterator[Path]:
"""Yield all HATCH/MPOLYGON boundary paths as separated :class:`Path` objects in OCS
coordinates. Elevation and offset is applied to all vertices.
.. versionadded:: 1.1
"""
elevation = hatch.dxf.elevation.z
for boundary in hatch.paths:
p = from_hatch_boundary_path(boundary, elevation=elevation, offset=offset)
if p.has_sub_paths:
yield from p.sub_paths()
else:
yield p
def from_hatch_boundary_path(
boundary: AbstractBoundaryPath,
ocs: Optional[OCS] = None,
elevation: float = 0,
offset: Vec3 = NULLVEC, # ocs offset!
) -> Path:
"""Returns a :class:`Path` object from a :class:`~ezdxf.entities.Hatch`
polyline- or edge path.
"""
if isinstance(boundary, EdgePath):
p = from_hatch_edge_path(boundary, ocs, elevation)
elif isinstance(boundary, PolylinePath):
p = from_hatch_polyline_path(boundary, ocs, elevation)
else:
raise TypeError(type(boundary))
if offset and ocs is not None: # only for MPOLYGON
# assume offset is in OCS
offset = ocs.to_wcs(offset.replace(z=elevation))
p = p.transform(Matrix44.translate(offset.x, offset.y, offset.z))
# attach path type information
p.user_data = const.BoundaryPathState.from_flags(boundary.path_type_flags)
return p
def from_hatch_polyline_path(
polyline: PolylinePath, ocs: Optional[OCS] = None, elevation: float = 0
) -> Path:
"""Returns a :class:`Path` object from a :class:`~ezdxf.entities.Hatch`
polyline path.
"""
path = Path()
tools.add_2d_polyline(
path,
polyline.vertices, # list[(x, y, bulge)]
close=polyline.is_closed,
ocs=ocs or OCS(),
elevation=elevation,
)
return path
def from_hatch_edge_path(
edges: EdgePath,
ocs: Optional[OCS] = None,
elevation: float = 0,
) -> Path:
"""Returns a :class:`Path` object from a :class:`~ezdxf.entities.Hatch`
edge path.
"""
def line(edge: LineEdge):
start = wcs(edge.start)
end = wcs(edge.end)
segment = Path(start)
segment.line_to(end)
return segment
def arc(edge: ArcEdge):
x, y, *_ = edge.center
# from_arc() requires OCS data:
# Note: clockwise oriented arcs are converted to counter
# clockwise arcs at the loading stage!
# See: ezdxf.entities.boundary_paths.ArcEdge.load_tags()
ellipse = ConstructionEllipse.from_arc(
center=(x, y, elevation),
radius=edge.radius,
extrusion=extrusion,
start_angle=edge.start_angle,
end_angle=edge.end_angle,
)
segment = Path()
tools.add_ellipse(segment, ellipse, reset=True)
return segment
def ellipse(edge: EllipseEdge):
ocs_ellipse = edge.construction_tool()
# ConstructionEllipse has WCS representation:
# Note: clockwise oriented ellipses are converted to counter
# clockwise ellipses at the loading stage!
# See: ezdxf.entities.boundary_paths.EllipseEdge.load_tags()
ellipse = ConstructionEllipse(
center=wcs(ocs_ellipse.center.replace(z=float(elevation))),
major_axis=wcs_tangent(ocs_ellipse.major_axis),
ratio=ocs_ellipse.ratio,
extrusion=extrusion,
start_param=ocs_ellipse.start_param,
end_param=ocs_ellipse.end_param,
)
segment = Path()
tools.add_ellipse(segment, ellipse, reset=True)
return segment
def spline(edge: SplineEdge):
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)
segment = Path()
tools.add_spline(segment, bspline, reset=True)
return segment
def from_fit_points(edge: SplineEdge, fit_points):
tangents = None
if edge.start_tangent and edge.end_tangent:
tangents = (
wcs_tangent(edge.start_tangent),
wcs_tangent(edge.end_tangent),
)
return fit_points_to_cad_cv( # only a degree of 3 is supported
fit_points,
tangents=tangents,
)
def from_control_points(edge: SplineEdge, 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: UVec) -> Vec3:
return _wcs(Vec3(vertex[0], vertex[1], elevation))
def wcs_tangent(vertex: UVec) -> Vec3:
return _wcs(Vec3(vertex[0], vertex[1], 0))
def _wcs(vec3: Vec3) -> Vec3:
if ocs and ocs.transform:
return ocs.to_wcs(vec3)
else:
return vec3
extrusion = ocs.uz if ocs else Z_AXIS
path = Path()
loop: Optional[Path] = None
for edge in edges:
next_segment: Optional[Path] = None
if isinstance(edge, LineEdge):
next_segment = line(edge)
elif isinstance(edge, ArcEdge):
if abs(edge.radius) > ABS_TOL:
next_segment = arc(edge)
elif isinstance(edge, EllipseEdge):
if not Vec2(edge.major_axis).is_null:
next_segment = ellipse(edge)
elif isinstance(edge, SplineEdge):
next_segment = spline(edge)
else:
raise TypeError(type(edge))
if next_segment is None:
continue
if loop is None:
loop = next_segment
continue
if loop.end.isclose(next_segment.start):
# end of current loop connects to the start of the next segment
loop.append_path(next_segment)
elif loop.end.isclose(next_segment.end):
# end of current loop connects to the end of the next segment
loop.append_path(next_segment.reversed())
elif loop.start.isclose(next_segment.end):
# start of the current loop connects to the end of the next segment
next_segment.append_path(loop)
loop = next_segment
elif loop.start.isclose(next_segment.start):
# start of the current loop connects to the start of the next segment
loop = loop.reversed()
loop.append_path(next_segment)
else: # gap between current loop and next segment
if loop.is_closed: # start a new loop
path.extend_multi_path(loop)
loop = next_segment # start a new loop
# behavior changed in version v0.18 based on issue #706:
else: # close the gap by a straight line and append the segment
loop.append_path(next_segment)
if loop is not None:
loop.close()
path.extend_multi_path(loop)
return path # multi path
def from_vertices(vertices: Iterable[UVec], close=False) -> Path:
"""Returns a :class:`Path` object from the given `vertices`."""
_vertices = Vec3.list(vertices)
if len(_vertices) < 2:
return Path()
path = Path(start=_vertices[0])
for vertex in _vertices[1:]:
if not path.end.isclose(vertex):
path.line_to(vertex)
if close:
path.close()
return path
def to_lwpolylines(
paths: Iterable[Path],
*,
distance: float = MAX_DISTANCE,
segments: int = MIN_SEGMENTS,
extrusion: UVec = Z_AXIS,
dxfattribs=None,
) -> Iterator[LWPolyline]:
"""Convert the given `paths` into :class:`~ezdxf.entities.LWPolyline`
entities.
The `extrusion` vector is applied to all paths, all vertices are projected
onto the plane normal to this extrusion vector. The default extrusion vector
is the WCS z-axis. The plane elevation is the distance from the WCS origin
to the start point of the first path.
Args:
paths: iterable of :class:`Path` objects
distance: maximum distance, see :meth:`Path.flattening`
segments: minimum segment count per Bézier curve
extrusion: extrusion vector for all paths
dxfattribs: additional DXF attribs
Returns:
iterable of :class:`~ezdxf.entities.LWPolyline` objects
"""
if isinstance(paths, Path):
paths = [paths]
else:
paths = list(paths)
if len(paths) == 0:
return
extrusion = Vec3(extrusion)
reference_point = Vec3(paths[0].start)
dxfattribs = dict(dxfattribs or {})
if not Z_AXIS.isclose(extrusion):
ocs, elevation = _get_ocs(extrusion, reference_point)
paths = tools.transform_paths_to_ocs(paths, ocs)
dxfattribs["elevation"] = elevation
dxfattribs["extrusion"] = extrusion
elif reference_point.z != 0:
dxfattribs["elevation"] = reference_point.z
for path in tools.single_paths(paths):
if len(path) > 0:
p = LWPolyline.new(dxfattribs=dxfattribs)
p.append_points(path.flattening(distance, segments), format="xy")
yield p
def _get_ocs(extrusion: Vec3, reference_point: Vec3) -> tuple[OCS, float]:
ocs = OCS(extrusion)
elevation = ocs.from_wcs(reference_point).z
return ocs, elevation
def to_polylines2d(
paths: Iterable[Path],
*,
distance: float = MAX_DISTANCE,
segments: int = MIN_SEGMENTS,
extrusion: UVec = Z_AXIS,
dxfattribs=None,
) -> Iterator[Polyline]:
"""Convert the given `paths` into 2D :class:`~ezdxf.entities.Polyline`
entities.
The `extrusion` vector is applied to all paths, all vertices are projected
onto the plane normal to this extrusion vector. The default extrusion vector
is the WCS z-axis. The plane elevation is the distance from the WCS origin
to the start point of the first path.
Args:
paths: iterable of :class:`Path` objects
distance: maximum distance, see :meth:`Path.flattening`
segments: minimum segment count per Bézier curve
extrusion: extrusion vector for all paths
dxfattribs: additional DXF attribs
Returns:
iterable of 2D :class:`~ezdxf.entities.Polyline` objects
"""
if isinstance(paths, Path):
paths = [paths]
else:
paths = list(paths)
if len(paths) == 0:
return
extrusion = Vec3(extrusion)
reference_point = Vec3(paths[0].start)
dxfattribs = dict(dxfattribs or {})
if not Z_AXIS.isclose(extrusion):
ocs, elevation = _get_ocs(extrusion, reference_point)
paths = tools.transform_paths_to_ocs(paths, ocs)
dxfattribs["elevation"] = Vec3(0, 0, elevation)
dxfattribs["extrusion"] = extrusion
elif reference_point.z != 0:
dxfattribs["elevation"] = Vec3(0, 0, reference_point.z)
for path in tools.single_paths(paths):
if len(path) > 0:
p = Polyline.new(dxfattribs=dxfattribs)
p.append_vertices(path.flattening(distance, segments))
p.new_seqend()
yield p
def to_hatches(
paths: Iterable[Path],
*,
edge_path: bool = True,
distance: float = MAX_DISTANCE,
segments: int = MIN_SEGMENTS,
g1_tol: float = G1_TOL,
extrusion: UVec = Z_AXIS,
dxfattribs=None,
) -> Iterator[Hatch]:
"""Convert the given `paths` into :class:`~ezdxf.entities.Hatch` entities.
Uses LWPOLYLINE paths for boundaries without curves and edge paths, build
of LINE and SPLINE edges, as boundary paths for boundaries including curves.
The `extrusion` vector is applied to all paths, all vertices are projected
onto the plane normal to this extrusion vector. The default extrusion vector
is the WCS z-axis. The plane elevation is the distance from the WCS origin
to the start point of the first path.
Args:
paths: iterable of :class:`Path` objects
edge_path: ``True`` for edge paths build of LINE and SPLINE edges,
``False`` for only LWPOLYLINE paths as boundary paths
distance: maximum distance, see :meth:`Path.flattening`
segments: minimum segment count per Bézier curve to flatten LWPOLYLINE paths
g1_tol: tolerance for G1 continuity check to separate SPLINE edges
extrusion: extrusion vector to all paths
dxfattribs: additional DXF attribs
Returns:
iterable of :class:`~ezdxf.entities.Hatch` objects
"""
boundary_factory: BoundaryFactory
if edge_path:
# noinspection PyTypeChecker
boundary_factory = partial(
build_edge_path, distance=distance, segments=segments, g1_tol=g1_tol
)
else:
# noinspection PyTypeChecker
boundary_factory = partial(
build_poly_path, distance=distance, segments=segments
)
yield from _polygon_converter(Hatch, paths, boundary_factory, extrusion, dxfattribs)
def to_mpolygons(
paths: Iterable[Path],
*,
distance: float = MAX_DISTANCE,
segments: int = MIN_SEGMENTS,
extrusion: UVec = Z_AXIS,
dxfattribs=None,
) -> Iterator[MPolygon]:
"""Convert the given `paths` into :class:`~ezdxf.entities.MPolygon` entities.
In contrast to HATCH, MPOLYGON supports only polyline boundary paths.
All curves will be approximated.
The `extrusion` vector is applied to all paths, all vertices are projected
onto the plane normal to this extrusion vector. The default extrusion vector
is the WCS z-axis. The plane elevation is the distance from the WCS origin
to the start point of the first path.
Args:
paths: iterable of :class:`Path` objects
distance: maximum distance, see :meth:`Path.flattening`
segments: minimum segment count per Bézier curve to flatten LWPOLYLINE paths
extrusion: extrusion vector to all paths
dxfattribs: additional DXF attribs
Returns:
iterable of :class:`~ezdxf.entities.MPolygon` objects
"""
# noinspection PyTypeChecker
boundary_factory: BoundaryFactory = partial(
build_poly_path, distance=distance, segments=segments
)
dxfattribs = dict(dxfattribs or {})
dxfattribs.setdefault("fill_color", const.BYLAYER)
yield from _polygon_converter(
MPolygon, paths, boundary_factory, extrusion, dxfattribs
)
def build_edge_path(
boundaries: BoundaryPaths,
path: Path,
flags: int,
distance: float,
segments: int,
g1_tol: float,
):
if path.has_curves: # Edge path with LINE and SPLINE edges
edge_path = boundaries.add_edge_path(flags)
for edge in to_bsplines_and_vertices(path, g1_tol=g1_tol):
if isinstance(edge, BSpline):
edge_path.add_spline(
control_points=edge.control_points,
degree=edge.degree,
knot_values=edge.knots(),
)
else: # add LINE edges
prev = edge[0]
for p in edge[1:]:
edge_path.add_line(prev, p)
prev = p
else: # Polyline boundary path
boundaries.add_polyline_path(
Vec2.generate(path.flattening(distance, segments)), flags=flags
)
def build_poly_path(
boundaries: BoundaryPaths,
path: Path,
flags: int,
distance: float,
segments: int,
):
boundaries.add_polyline_path(
# Vec2 removes the z-axis, which would be interpreted as bulge value!
Vec2.generate(path.flattening(distance, segments)),
flags=flags,
)
def _polygon_converter(
cls: Type[TPolygon],
paths: Iterable[Path],
add_boundary: BoundaryFactory,
extrusion: UVec = Z_AXIS,
dxfattribs=None,
) -> Iterator[TPolygon]:
if isinstance(paths, Path):
paths = [paths]
else:
paths = list(paths)
if len(paths) == 0:
return
extrusion = Vec3(extrusion)
reference_point = paths[0].start
_dxfattribs: dict = dict(dxfattribs or {})
if not Z_AXIS.isclose(extrusion):
ocs, elevation = _get_ocs(extrusion, reference_point)
paths = tools.transform_paths_to_ocs(paths, ocs)
_dxfattribs["elevation"] = Vec3(0, 0, elevation)
_dxfattribs["extrusion"] = extrusion
elif reference_point.z != 0:
_dxfattribs["elevation"] = Vec3(0, 0, reference_point.z)
_dxfattribs.setdefault("solid_fill", 1)
_dxfattribs.setdefault("pattern_name", "SOLID")
_dxfattribs.setdefault("color", const.BYLAYER)
for group in group_paths(tools.single_paths(paths)):
if len(group) == 0:
continue
polygon = cls.new(dxfattribs=_dxfattribs)
boundaries = polygon.paths
external = group[0]
external.close()
add_boundary(boundaries, external, 1)
for hole in group[1:]:
hole.close()
add_boundary(boundaries, hole, 0)
yield polygon
def to_polylines3d(
paths: Iterable[Path],
*,
distance: float = MAX_DISTANCE,
segments: int = MIN_SEGMENTS,
dxfattribs=None,
) -> Iterator[Polyline]:
"""Convert the given `paths` into 3D :class:`~ezdxf.entities.Polyline`
entities.
Args:
paths: iterable of :class:`Path` objects
distance: maximum distance, see :meth:`Path.flattening`
segments: minimum segment count per Bézier curve
dxfattribs: additional DXF attribs
Returns:
iterable of 3D :class:`~ezdxf.entities.Polyline` objects
"""
if isinstance(paths, Path):
paths = [paths]
dxfattribs = dict(dxfattribs or {})
dxfattribs["flags"] = const.POLYLINE_3D_POLYLINE
for path in tools.single_paths(paths):
if len(path) > 0:
p = Polyline.new(dxfattribs=dxfattribs)
p.append_vertices(path.flattening(distance, segments))
p.new_seqend()
yield p
def to_lines(
paths: Iterable[Path],
*,
distance: float = MAX_DISTANCE,
segments: int = MIN_SEGMENTS,
dxfattribs=None,
) -> Iterator[Line]:
"""Convert the given `paths` into :class:`~ezdxf.entities.Line` entities.
Args:
paths: iterable of :class:`Path` objects
distance: maximum distance, see :meth:`Path.flattening`
segments: minimum segment count per Bézier curve
dxfattribs: additional DXF attribs
Returns:
iterable of :class:`~ezdxf.entities.Line` objects
"""
if isinstance(paths, Path):
paths = [paths]
dxfattribs = dict(dxfattribs or {})
prev_vertex = None
for path in tools.single_paths(paths):
if len(path) == 0:
continue
for vertex in path.flattening(distance, segments):
if prev_vertex is None:
prev_vertex = vertex
continue
dxfattribs["start"] = prev_vertex
dxfattribs["end"] = vertex
yield Line.new(dxfattribs=dxfattribs)
prev_vertex = vertex
prev_vertex = None
PathParts: TypeAlias = Union[BSpline, List[Vec3]]
def to_bsplines_and_vertices(path: Path, g1_tol: float = G1_TOL) -> Iterator[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`
"""
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)
curves = []
for path in tools.single_paths([path]):
prev = path.start
for cmd in path:
if cmd.type == Command.CURVE3_TO:
curve = Bezier3P([prev, cmd.ctrl, cmd.end]) # type: ignore
elif cmd.type == Command.CURVE4_TO:
curve = Bezier4P([prev, cmd.ctrl1, cmd.ctrl2, cmd.end]) # type: ignore
elif cmd.type == Command.LINE_TO:
curve = (prev, cmd.end)
else:
raise ValueError
curves.append(curve)
prev = cmd.end
bezier: list = []
polyline: list = []
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 to_splines_and_polylines(
paths: Iterable[Path],
*,
g1_tol: float = G1_TOL,
dxfattribs=None,
) -> Iterator[Union[Spline, Polyline]]:
"""Convert the given `paths` into :class:`~ezdxf.entities.Spline` and 3D
:class:`~ezdxf.entities.Polyline` entities.
Args:
paths: iterable of :class:`Path` objects
g1_tol: tolerance for G1 continuity check
dxfattribs: additional DXF attribs
Returns:
iterable of :class:`~ezdxf.entities.Line` objects
"""
if isinstance(paths, Path):
paths = [paths]
dxfattribs = dict(dxfattribs or {})
for path in tools.single_paths(paths):
for data in to_bsplines_and_vertices(path, g1_tol):
if isinstance(data, BSpline):
spline = Spline.new(dxfattribs=dxfattribs)
spline.apply_construction_tool(data)
yield spline
else:
attribs = dict(dxfattribs)
attribs["flags"] = const.POLYLINE_3D_POLYLINE
polyline = Polyline.new(dxfattribs=dxfattribs)
polyline.append_vertices(data)
polyline.new_seqend()
yield polyline