Source code for cadquery.occ_impl.exporters.dxf

"""DXF export utilities."""

from typing import (
    Any,
    Dict,
    List,
    Literal,
    Optional,
    Tuple,
    Union,
    Iterable,
    Protocol,
    runtime_checkable,
)

import ezdxf
from ezdxf import units, zoom
from ezdxf.entities import factory
from OCP.GeomConvert import GeomConvert
from OCP.gp import gp_Dir
from OCP.GC import GC_MakeArcOfEllipse
from typing_extensions import Self

from ...units import RAD2DEG
from ..shapes import Face, Edge, Shape, Compound, compound
from ..geom import Plane


ApproxOptions = Literal["spline", "arc"]
DxfEntityAttributes = Tuple[
    Literal["ARC", "CIRCLE", "ELLIPSE", "LINE", "SPLINE",], Dict[str, Any]
]


@runtime_checkable
class WorkplaneLike(Protocol):
    @property
    def plane(self) -> Plane:
        ...

    def __iter__(self) -> Iterable[Shape]:
        ...


[docs] class DxfDocument: """Create DXF document from CadQuery objects. A wrapper for `ezdxf <https://ezdxf.readthedocs.io/>`_ providing methods for converting :class:`cadquery.Workplane` objects to DXF entities. The ezdxf document is available as the property ``document``, allowing most features of ezdxf to be utilised directly. .. rubric:: Example usage .. code-block:: python :caption: Single layer DXF document rectangle = cq.Workplane().rect(10, 20) dxf = DxfDocument() dxf.add_shape(rectangle) dxf.document.saveas("rectangle.dxf") .. code-block:: python :caption: Multilayer DXF document rectangle = cq.Workplane().rect(10, 20) circle = cq.Workplane().circle(3) dxf = DxfDocument() dxf = ( dxf.add_layer("layer_1", color=2) .add_layer("layer_2", color=3) .add_shape(rectangle, "layer_1") .add_shape(circle, "layer_2") ) dxf.document.saveas("rectangle-with-hole.dxf") """ CURVE_TOLERANCE = 1e-9
[docs] def __init__( self, dxfversion: str = "AC1027", setup: Union[bool, List[str]] = False, doc_units: int = units.MM, *, metadata: Union[Dict[str, str], None] = None, approx: Optional[ApproxOptions] = None, tolerance: float = 1e-3, ): """Initialize DXF document. :param dxfversion: :attr:`DXF version specifier <ezdxf-stable:ezdxf.document.Drawing.dxfversion>` as string, default is "AC1027" respectively "R2013" :param setup: setup default styles, ``False`` for no setup, ``True`` to set up everything or a list of topics as strings, e.g. ``["linetypes", "styles"]`` refer to :func:`ezdxf-stable:ezdxf.new`. :param doc_units: ezdxf document/modelspace :doc:`units <ezdxf-stable:concepts/units>` :param metadata: document :ref:`metadata <ezdxf-stable:ezdxf_metadata>` a dictionary of name value pairs :param approx: Approximation strategy for converting :class:`cadquery.Workplane` objects to DXF entities: ``None`` no approximation applied ``"spline"`` all splines approximated as cubic splines ``"arc"`` all curves approximated as arcs and straight segments :param tolerance: Approximation tolerance for converting :class:`cadquery.Workplane` objects to DXF entities. """ if metadata is None: metadata = {} self._DISPATCH_MAP = { "LINE": self._dxf_line, "CIRCLE": self._dxf_circle, "ELLIPSE": self._dxf_ellipse, } self.approx = approx self.tolerance = tolerance self.document = ezdxf.new(dxfversion=dxfversion, setup=setup, units=doc_units) # type: ignore[attr-defined] self.msp = self.document.modelspace() doc_metadata = self.document.ezdxf_metadata() for key, value in metadata.items(): doc_metadata[key] = value
[docs] def add_layer( self, name: str, *, color: int = 7, linetype: str = "CONTINUOUS" ) -> Self: """Create a layer definition Refer to :ref:`ezdxf layers <ezdxf-stable:layer_concept>` and :doc:`ezdxf layer tutorial <ezdxf-stable:tutorials/layers>`. :param name: layer definition name :param color: color index. Standard colors include: 1 red, 2 yellow, 3 green, 4 cyan, 5 blue, 6 magenta, 7 white/black :param linetype: ezdxf :doc:`line type <ezdxf-stable:concepts/linetypes>` """ self.document.layers.add(name, color=color, linetype=linetype) return self
[docs] def add_shape(self, shape: Union[WorkplaneLike, Shape], layer: str = "") -> Self: """Add CadQuery shape to a DXF layer. :param s: CadQuery Workplane or Shape :param layer: layer definition name """ if isinstance(shape, WorkplaneLike): plane = shape.plane shape_ = compound(*shape).transformShape(plane.fG) else: shape_ = shape general_attributes = {} if layer: general_attributes["layer"] = layer if self.approx == "spline": edges = [ e.toSplines() if e.geomType() == "BSPLINE" else e for e in self._ordered_edges(shape_) ] elif self.approx == "arc": edges = [] # this is needed to handle free wires for el in shape_.Wires(): edges.extend( self._ordered_edges(Face.makeFromWires(el).toArcs(self.tolerance)) ) else: edges = self._ordered_edges(shape_) for edge in edges: converter = self._DISPATCH_MAP.get(edge.geomType(), None) if converter: entity_type, entity_attributes = converter(edge) entity = factory.new( entity_type, dxfattribs={**entity_attributes, **general_attributes} ) self.msp.add_entity(entity) # type: ignore[arg-type] else: _, entity_attributes = self._dxf_spline(edge, plane) entity = ezdxf.math.BSpline(**entity_attributes) # type: ignore[assignment] self.msp.add_spline( dxfattribs=general_attributes ).apply_construction_tool(entity) return self
@staticmethod def _ordered_edges(s: Shape) -> List[Edge]: rv: List[Edge] = [] # iterate over wires and then edges for w in s.Wires(): rv.extend(w) # add free edges if isinstance(s, Compound): rv.extend(e for e in s if isinstance(e, Edge)) return rv @staticmethod def _dxf_line(edge: Edge) -> DxfEntityAttributes: """Convert a Line to DXF entity attributes. :param edge: CadQuery Edge to be converted to a DXF line :return: dictionary of DXF entity attributes for creating a line """ return ( "LINE", {"start": edge.startPoint().toTuple(), "end": edge.endPoint().toTuple(),}, ) @staticmethod def _dxf_circle(edge: Edge) -> DxfEntityAttributes: """Convert a Circle to DXF entity attributes. :param edge: CadQuery Edge to be converted to a DXF circle :return: dictionary of DXF entity attributes for creating either a circle or arc """ geom = edge._geomAdaptor() circ = geom.Circle() radius = circ.Radius() location = circ.Location() direction_y = circ.YAxis().Direction() direction_z = circ.Axis().Direction() dy = gp_Dir(0, 1, 0) phi = direction_y.AngleWithRef(dy, direction_z) if direction_z.XYZ().Z() > 0: a1 = RAD2DEG * (geom.FirstParameter() - phi) a2 = RAD2DEG * (geom.LastParameter() - phi) else: a1 = -RAD2DEG * (geom.LastParameter() - phi) + 180 a2 = -RAD2DEG * (geom.FirstParameter() - phi) + 180 if edge.IsClosed(): return ( "CIRCLE", { "center": (location.X(), location.Y(), location.Z()), "radius": radius, }, ) else: return ( "ARC", { "center": (location.X(), location.Y(), location.Z()), "radius": radius, "start_angle": a1, "end_angle": a2, }, ) @staticmethod def _dxf_ellipse(edge: Edge) -> DxfEntityAttributes: """Convert an Ellipse to DXF entity attributes. :param edge: CadQuery Edge to be converted to a DXF ellipse :return: dictionary of DXF entity attributes for creating an ellipse """ geom = edge._geomAdaptor() ellipse = geom.Ellipse() r1 = ellipse.MinorRadius() r2 = ellipse.MajorRadius() c = ellipse.Location() xdir = ellipse.XAxis().Direction() xax = r2 * xdir.XYZ() zdir = ellipse.Axis().Direction() if zdir.Z() > 0: start_param = geom.FirstParameter() end_param = geom.LastParameter() else: gc = GC_MakeArcOfEllipse( ellipse, geom.FirstParameter(), geom.LastParameter(), False, # reverse Sense ).Value() start_param = gc.FirstParameter() end_param = gc.LastParameter() return ( "ELLIPSE", { "center": (c.X(), c.Y(), c.Z()), "major_axis": (xax.X(), xax.Y(), xax.Z()), "ratio": r1 / r2, "start_param": start_param, "end_param": end_param, }, ) @classmethod def _dxf_spline(cls, edge: Edge, plane: Plane) -> DxfEntityAttributes: """Convert a Spline to ezdxf.math.BSpline parameters. :param edge: CadQuery Edge to be converted to a DXF spline :param plane: CadQuery Plane :return: dictionary of ezdxf.math.BSpline parameters """ adaptor = edge._geomAdaptor() curve = GeomConvert.CurveToBSplineCurve_s(adaptor.Curve().Curve()) spline = GeomConvert.SplitBSplineCurve_s( curve, adaptor.FirstParameter(), adaptor.LastParameter(), cls.CURVE_TOLERANCE, ) # need to apply the transform on the geometry level spline.Transform(adaptor.Trsf()) order = spline.Degree() + 1 knots = list(spline.KnotSequence()) poles = [(p.X(), p.Y(), p.Z()) for p in spline.Poles()] weights = ( [spline.Weight(i) for i in range(1, spline.NbPoles() + 1)] if spline.IsRational() else None ) if spline.IsPeriodic(): pad = spline.NbKnots() - spline.LastUKnotIndex() poles += poles[:pad] return ( "SPLINE", { "control_points": poles, "order": order, "knots": knots, "weights": weights, }, )
def exportDXF( w: Union[WorkplaneLike, Shape, Iterable[Shape]], fname: str, approx: Optional[ApproxOptions] = None, tolerance: float = 1e-3, *, doc_units: int = units.MM, ) -> None: """ Export Workplane content to DXF. Works with 2D sections. :param w: Workplane to be exported. :param fname: Output filename. :param approx: Approximation strategy. None means no approximation is applied. "spline" results in all splines being approximated as cubic splines. "arc" results in all curves being approximated as arcs and straight segments. :param tolerance: Approximation tolerance. :param doc_units: ezdxf document/modelspace :doc:`units <ezdxf-stable:concepts/units>` (in. = ``1``, mm = ``4``). """ dxf = DxfDocument(approx=approx, tolerance=tolerance, doc_units=doc_units) if isinstance(w, (WorkplaneLike, Shape)): dxf.add_shape(w) else: for s in w: dxf.add_shape(s) zoom.extents(dxf.msp) dxf.document.saveas(fname)