961 lines
30 KiB
Python
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
|