DXF multilayer support (#1267)
* Test DXF export with text * DXF multilayer support Also supports: * setting units * setting color by layer * setting line type by layer * Apply transformation to bspline curve on DXF export * Typing improvements * Improve test coverage --------- Co-authored-by: Lorenz Neureuter <hello@lorenz.space>
This commit is contained in:
@ -15,7 +15,7 @@ from .svg import getSVG
|
|||||||
from .json import JsonMesh
|
from .json import JsonMesh
|
||||||
from .amf import AmfWriter
|
from .amf import AmfWriter
|
||||||
from .threemf import ThreeMFWriter
|
from .threemf import ThreeMFWriter
|
||||||
from .dxf import exportDXF
|
from .dxf import exportDXF, DxfDocument
|
||||||
from .vtk import exportVTP
|
from .vtk import exportVTP
|
||||||
from .utils import toCompound
|
from .utils import toCompound
|
||||||
|
|
||||||
|
@ -1,56 +1,246 @@
|
|||||||
from ...cq import Workplane, Plane, Face
|
"""DXF export utilities."""
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import ezdxf
|
||||||
|
from ezdxf import units, zoom
|
||||||
|
from ezdxf.entities import factory
|
||||||
|
from OCP.GeomConvert import GeomConvert
|
||||||
|
from OCP.gp import gp_Dir
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from ...cq import Face, Plane, Workplane
|
||||||
from ...units import RAD2DEG
|
from ...units import RAD2DEG
|
||||||
from ..shapes import Edge
|
from ..shapes import Edge
|
||||||
from .utils import toCompound
|
from .utils import toCompound
|
||||||
|
|
||||||
from OCP.gp import gp_Dir
|
ApproxOptions = Literal["spline", "arc"]
|
||||||
from OCP.GeomConvert import GeomConvert
|
DxfEntityAttributes = Tuple[
|
||||||
|
Literal["ARC", "CIRCLE", "ELLIPSE", "LINE", "SPLINE",], Dict[str, Any]
|
||||||
from typing import Optional, Literal
|
]
|
||||||
|
|
||||||
import ezdxf
|
|
||||||
|
|
||||||
CURVE_TOLERANCE = 1e-9
|
|
||||||
|
|
||||||
|
|
||||||
def _dxf_line(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
|
class DxfDocument:
|
||||||
|
"""Create DXF document from CadQuery objects.
|
||||||
|
|
||||||
msp.add_line(
|
A wrapper for `ezdxf <https://ezdxf.readthedocs.io/>`_ providing methods for
|
||||||
e.startPoint().toTuple(), e.endPoint().toTuple(),
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def add_shape(self, workplane: Workplane, layer: str = "") -> Self:
|
||||||
|
"""Add CadQuery shape to a DXF layer.
|
||||||
|
|
||||||
|
:param workplane: CadQuery Workplane
|
||||||
|
:param layer: layer definition name
|
||||||
|
"""
|
||||||
|
plane = workplane.plane
|
||||||
|
shape = toCompound(workplane).transformShape(plane.fG)
|
||||||
|
|
||||||
|
general_attributes = {}
|
||||||
|
if layer:
|
||||||
|
general_attributes["layer"] = layer
|
||||||
|
|
||||||
|
if self.approx == "spline":
|
||||||
|
edges = [
|
||||||
|
e.toSplines() if e.geomType() == "BSPLINE" else e for e in shape.Edges()
|
||||||
|
]
|
||||||
|
|
||||||
|
elif self.approx == "arc":
|
||||||
|
edges = []
|
||||||
|
|
||||||
|
# this is needed to handle free wires
|
||||||
|
for el in shape.Wires():
|
||||||
|
edges.extend(Face.makeFromWires(el).toArcs(self.tolerance).Edges())
|
||||||
|
|
||||||
|
else:
|
||||||
|
edges = shape.Edges()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
zoom.extents(self.msp)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
@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.
|
||||||
|
|
||||||
def _dxf_circle(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
|
:param edge: CadQuery Edge to be converted to a DXF circle
|
||||||
|
|
||||||
geom = e._geomAdaptor()
|
:return: dictionary of DXF entity attributes for creating either a circle or arc
|
||||||
|
"""
|
||||||
|
geom = edge._geomAdaptor()
|
||||||
circ = geom.Circle()
|
circ = geom.Circle()
|
||||||
|
|
||||||
r = circ.Radius()
|
radius = circ.Radius()
|
||||||
c = circ.Location()
|
location = circ.Location()
|
||||||
|
|
||||||
c_dy = circ.YAxis().Direction()
|
direction_y = circ.YAxis().Direction()
|
||||||
c_dz = circ.Axis().Direction()
|
direction_z = circ.Axis().Direction()
|
||||||
|
|
||||||
dy = gp_Dir(0, 1, 0)
|
dy = gp_Dir(0, 1, 0)
|
||||||
|
|
||||||
phi = c_dy.AngleWithRef(dy, c_dz)
|
phi = direction_y.AngleWithRef(dy, direction_z)
|
||||||
|
|
||||||
if c_dz.XYZ().Z() > 0:
|
if direction_z.XYZ().Z() > 0:
|
||||||
a1 = RAD2DEG * (geom.FirstParameter() - phi)
|
a1 = RAD2DEG * (geom.FirstParameter() - phi)
|
||||||
a2 = RAD2DEG * (geom.LastParameter() - phi)
|
a2 = RAD2DEG * (geom.LastParameter() - phi)
|
||||||
else:
|
else:
|
||||||
a1 = -RAD2DEG * (geom.LastParameter() - phi) + 180
|
a1 = -RAD2DEG * (geom.LastParameter() - phi) + 180
|
||||||
a2 = -RAD2DEG * (geom.FirstParameter() - phi) + 180
|
a2 = -RAD2DEG * (geom.FirstParameter() - phi) + 180
|
||||||
|
|
||||||
if e.IsClosed():
|
if edge.IsClosed():
|
||||||
msp.add_circle((c.X(), c.Y(), c.Z()), r)
|
return (
|
||||||
|
"CIRCLE",
|
||||||
|
{
|
||||||
|
"center": (location.X(), location.Y(), location.Z()),
|
||||||
|
"radius": radius,
|
||||||
|
},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
msp.add_arc((c.X(), c.Y(), c.Z()), r, a1, a2)
|
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.
|
||||||
|
|
||||||
def _dxf_ellipse(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
|
:param edge: CadQuery Edge to be converted to a DXF ellipse
|
||||||
|
|
||||||
geom = e._geomAdaptor()
|
:return: dictionary of DXF entity attributes for creating an ellipse
|
||||||
|
"""
|
||||||
|
geom = edge._geomAdaptor()
|
||||||
ellipse = geom.Ellipse()
|
ellipse = geom.Ellipse()
|
||||||
|
|
||||||
r1 = ellipse.MinorRadius()
|
r1 = ellipse.MinorRadius()
|
||||||
@ -60,26 +250,38 @@ def _dxf_ellipse(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
|
|||||||
xdir = ellipse.XAxis().Direction()
|
xdir = ellipse.XAxis().Direction()
|
||||||
xax = r2 * xdir.XYZ()
|
xax = r2 * xdir.XYZ()
|
||||||
|
|
||||||
msp.add_ellipse(
|
return (
|
||||||
(c.X(), c.Y(), c.Z()),
|
"ELLIPSE",
|
||||||
(xax.X(), xax.Y(), xax.Z()),
|
{
|
||||||
r1 / r2,
|
"center": (c.X(), c.Y(), c.Z()),
|
||||||
geom.FirstParameter(),
|
"major_axis": (xax.X(), xax.Y(), xax.Z()),
|
||||||
geom.LastParameter(),
|
"ratio": r1 / r2,
|
||||||
|
"start_param": geom.FirstParameter(),
|
||||||
|
"end_param": geom.LastParameter(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _dxf_spline(cls, edge: Edge, plane: Plane) -> DxfEntityAttributes:
|
||||||
|
"""Convert a Spline to ezdxf.math.BSpline parameters.
|
||||||
|
|
||||||
def _dxf_spline(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
|
:param edge: CadQuery Edge to be converted to a DXF spline
|
||||||
|
:param plane: CadQuery Plane
|
||||||
|
|
||||||
adaptor = e._geomAdaptor()
|
:return: dictionary of ezdxf.math.BSpline parameters
|
||||||
|
"""
|
||||||
|
adaptor = edge._geomAdaptor()
|
||||||
curve = GeomConvert.CurveToBSplineCurve_s(adaptor.Curve().Curve())
|
curve = GeomConvert.CurveToBSplineCurve_s(adaptor.Curve().Curve())
|
||||||
|
|
||||||
spline = GeomConvert.SplitBSplineCurve_s(
|
spline = GeomConvert.SplitBSplineCurve_s(
|
||||||
curve, adaptor.FirstParameter(), adaptor.LastParameter(), CURVE_TOLERANCE
|
curve,
|
||||||
|
adaptor.FirstParameter(),
|
||||||
|
adaptor.LastParameter(),
|
||||||
|
cls.CURVE_TOLERANCE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# need to apply the transform on the geometry level
|
# need to apply the transform on the geometry level
|
||||||
spline.Transform(plane.fG.wrapped.Trsf())
|
spline.Transform(adaptor.Trsf())
|
||||||
|
|
||||||
order = spline.Degree() + 1
|
order = spline.Degree() + 1
|
||||||
knots = list(spline.KnotSequence())
|
knots = list(spline.KnotSequence())
|
||||||
@ -94,25 +296,25 @@ def _dxf_spline(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
|
|||||||
pad = spline.NbKnots() - spline.LastUKnotIndex()
|
pad = spline.NbKnots() - spline.LastUKnotIndex()
|
||||||
poles += poles[:pad]
|
poles += poles[:pad]
|
||||||
|
|
||||||
dxf_spline = ezdxf.math.BSpline(poles, order, knots, weights)
|
return (
|
||||||
|
"SPLINE",
|
||||||
msp.add_spline().apply_construction_tool(dxf_spline)
|
{
|
||||||
|
"control_points": poles,
|
||||||
|
"order": order,
|
||||||
DXF_CONVERTERS = {
|
"knots": knots,
|
||||||
"LINE": _dxf_line,
|
"weights": weights,
|
||||||
"CIRCLE": _dxf_circle,
|
},
|
||||||
"ELLIPSE": _dxf_ellipse,
|
)
|
||||||
"BSPLINE": _dxf_spline,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def exportDXF(
|
def exportDXF(
|
||||||
w: Workplane,
|
w: Workplane,
|
||||||
fname: str,
|
fname: str,
|
||||||
approx: Optional[Literal["spline", "arc"]] = None,
|
approx: Optional[ApproxOptions] = None,
|
||||||
tolerance: float = 1e-3,
|
tolerance: float = 1e-3,
|
||||||
):
|
*,
|
||||||
|
doc_units: int = units.MM,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Export Workplane content to DXF. Works with 2D sections.
|
Export Workplane content to DXF. Works with 2D sections.
|
||||||
|
|
||||||
@ -122,33 +324,9 @@ def exportDXF(
|
|||||||
"spline" results in all splines being approximated as cubic splines. "arc" results
|
"spline" results in all splines being approximated as cubic splines. "arc" results
|
||||||
in all curves being approximated as arcs and straight segments.
|
in all curves being approximated as arcs and straight segments.
|
||||||
:param tolerance: Approximation tolerance.
|
:param tolerance: Approximation tolerance.
|
||||||
|
:param doc_units: ezdxf document/modelspace :doc:`units <ezdxf-stable:concepts/units>` (in. = ``1``, mm = ``4``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plane = w.plane
|
dxf = DxfDocument(approx=approx, tolerance=tolerance, doc_units=doc_units)
|
||||||
shape = toCompound(w).transformShape(plane.fG)
|
dxf.add_shape(w)
|
||||||
|
dxf.document.saveas(fname)
|
||||||
dxf = ezdxf.new()
|
|
||||||
msp = dxf.modelspace()
|
|
||||||
|
|
||||||
if approx == "spline":
|
|
||||||
edges = [
|
|
||||||
e.toSplines() if e.geomType() == "BSPLINE" else e for e in shape.Edges()
|
|
||||||
]
|
|
||||||
|
|
||||||
elif approx == "arc":
|
|
||||||
edges = []
|
|
||||||
|
|
||||||
# this is needed to handle free wires
|
|
||||||
for el in shape.Wires():
|
|
||||||
edges.extend(Face.makeFromWires(el).toArcs(tolerance).Edges())
|
|
||||||
|
|
||||||
else:
|
|
||||||
edges = shape.Edges()
|
|
||||||
|
|
||||||
for e in edges:
|
|
||||||
|
|
||||||
conv = DXF_CONVERTERS.get(e.geomType(), _dxf_spline)
|
|
||||||
conv(e, msp, plane)
|
|
||||||
|
|
||||||
dxf.saveas(fname)
|
|
||||||
|
@ -206,6 +206,7 @@ File Management and Export
|
|||||||
importers.importStep
|
importers.importStep
|
||||||
importers.importDXF
|
importers.importDXF
|
||||||
exporters.export
|
exporters.export
|
||||||
|
occ_impl.exporters.dxf.DxfDocument
|
||||||
|
|
||||||
|
|
||||||
Iteration Methods
|
Iteration Methods
|
||||||
|
@ -101,3 +101,8 @@ Class Details
|
|||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autofunction:: cadquery.occ_impl.assembly.toJSON
|
.. autofunction:: cadquery.occ_impl.assembly.toJSON
|
||||||
|
|
||||||
|
.. autoclass:: cadquery.occ_impl.exporters.dxf.DxfDocument
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. automethod:: __init__
|
||||||
|
@ -38,6 +38,7 @@ extensions = [
|
|||||||
"sphinx.ext.autodoc",
|
"sphinx.ext.autodoc",
|
||||||
"sphinx.ext.viewcode",
|
"sphinx.ext.viewcode",
|
||||||
"sphinx.ext.autosummary",
|
"sphinx.ext.autosummary",
|
||||||
|
"sphinx.ext.intersphinx",
|
||||||
"cadquery.cq_directive",
|
"cadquery.cq_directive",
|
||||||
"sphinx.ext.mathjax",
|
"sphinx.ext.mathjax",
|
||||||
"sphinx_autodoc_multimethod",
|
"sphinx_autodoc_multimethod",
|
||||||
@ -212,6 +213,11 @@ html_show_sphinx = False
|
|||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = "CadQuerydoc"
|
htmlhelp_basename = "CadQuerydoc"
|
||||||
|
|
||||||
|
# -- Options for intersphinx --------------------------------------------------
|
||||||
|
|
||||||
|
intersphinx_mapping = {
|
||||||
|
"ezdxf-stable": ("https://ezdxf.readthedocs.io/en/stable/", None),
|
||||||
|
}
|
||||||
|
|
||||||
# -- Options for LaTeX output --------------------------------------------------
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
|
@ -239,17 +239,99 @@ optimum values that will produce an acceptable mesh.
|
|||||||
Exporting DXF
|
Exporting DXF
|
||||||
##############
|
##############
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:class:`cadquery.occ_impl.exporters.dxf.DxfDocument` for exporting multiple
|
||||||
|
Workplanes to one or many layers of a DXF document.
|
||||||
|
|
||||||
|
Options
|
||||||
|
-------
|
||||||
|
|
||||||
|
``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
|
||||||
|
``tolerance``
|
||||||
|
Approximation tolerance for converting :class:`cadquery.Workplane` objects to DXF entities.
|
||||||
|
See `Approximation strategy`_.
|
||||||
|
``doc_units``
|
||||||
|
Ezdxf document/modelspace :doc:`units <ezdxf-stable:concepts/units>`.
|
||||||
|
See `Units`_.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: DXF document without options.
|
||||||
|
|
||||||
|
import cadquery as cq
|
||||||
|
from cadquery import exporters
|
||||||
|
|
||||||
|
result = cq.Workplane().box(10, 10, 10)
|
||||||
|
|
||||||
|
exporters.exportDXF(result, "/path/to/file/object.dxf")
|
||||||
|
# or
|
||||||
|
exporters.export(result, "/path/to/file/object.dxf")
|
||||||
|
|
||||||
|
|
||||||
|
Units
|
||||||
|
-----
|
||||||
|
|
||||||
|
The default DXF document units are mm (:code:`doc_units = 4`).
|
||||||
|
|
||||||
|
========= ===============
|
||||||
|
doc_units Unit
|
||||||
|
========= ===============
|
||||||
|
0 Unitless
|
||||||
|
1 Inches
|
||||||
|
2 Feet
|
||||||
|
3 Miles
|
||||||
|
4 Millimeters
|
||||||
|
5 Centimeters
|
||||||
|
6 Meters
|
||||||
|
========= ===============
|
||||||
|
|
||||||
|
Document units can be set to any :doc:`unit supported by ezdxf <ezdxf-stable:concepts/units>`.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:caption: DXF document with units set to meters.
|
||||||
|
|
||||||
|
import cadquery as cq
|
||||||
|
from cadquery import exporters
|
||||||
|
|
||||||
|
result = cq.Workplane().box(10, 10, 10)
|
||||||
|
|
||||||
|
exporters.exportDXF(
|
||||||
|
result, "/path/to/file/object.dxf", doc_units=6, # set DXF document units to meters
|
||||||
|
)
|
||||||
|
|
||||||
|
# or
|
||||||
|
|
||||||
|
exporters.export(
|
||||||
|
result,
|
||||||
|
"/path/to/file/object.dxf",
|
||||||
|
opt={"doc_units": 6}, # set DXF document units to meters
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
.. _Approximation strategy:
|
||||||
|
|
||||||
|
Approximation strategy
|
||||||
|
----------------------
|
||||||
|
|
||||||
By default, the DXF exporter will output splines exactly as they are represented by the OpenCascade kernel. Unfortunately some software cannot handle higher-order splines resulting in missing curves after DXF import. To resolve this, specify an approximation strategy controlled by the following options:
|
By default, the DXF exporter will output splines exactly as they are represented by the OpenCascade kernel. Unfortunately some software cannot handle higher-order splines resulting in missing curves after DXF import. To resolve this, specify an approximation strategy controlled by the following options:
|
||||||
|
|
||||||
* ``approx`` - ``None``, ``"spline"`` or ``"arc"``. ``"spline"`` results in all splines approximated with cubic splines. ``"arc"`` results in all curves approximated with arcs and line segments.
|
* ``approx`` - ``None``, ``"spline"`` or ``"arc"``. ``"spline"`` results in all splines approximated with cubic splines. ``"arc"`` results in all curves approximated with arcs and line segments.
|
||||||
* ``tolerance``: Acceptable error of the approximation, in the DXF's coordinate system. Defaults to 0.001 (1 thou for inch-scale drawings, 1 µm for mm-scale drawings).
|
* ``tolerance``: Acceptable error of the approximation, in document/modelspace units. Defaults to 0.001 (1 thou for inch-scale drawings, 1 µm for mm-scale drawings).
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
:caption: DXF document with curves approximated with cubic splines.
|
||||||
|
|
||||||
cq.exporters.exportDXF(
|
cq.exporters.exportDXF(
|
||||||
result,
|
result,
|
||||||
'/path/to/file/object.dxf',
|
"/path/to/file/object.dxf",
|
||||||
approx="spline"
|
approx="spline"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Tests basic workplane functionality
|
Tests exporters
|
||||||
"""
|
"""
|
||||||
# core modules
|
# core modules
|
||||||
import os
|
import os
|
||||||
@ -16,6 +16,7 @@ from pytest import approx
|
|||||||
from cadquery import (
|
from cadquery import (
|
||||||
exporters,
|
exporters,
|
||||||
importers,
|
importers,
|
||||||
|
Sketch,
|
||||||
Workplane,
|
Workplane,
|
||||||
Edge,
|
Edge,
|
||||||
Vertex,
|
Vertex,
|
||||||
@ -24,6 +25,8 @@ from cadquery import (
|
|||||||
Location,
|
Location,
|
||||||
Vector,
|
Vector,
|
||||||
)
|
)
|
||||||
|
from cadquery.occ_impl.exporters.dxf import DxfDocument
|
||||||
|
from cadquery.occ_impl.exporters.utils import toCompound
|
||||||
from tests import BaseTest
|
from tests import BaseTest
|
||||||
from OCP.GeomConvert import GeomConvert
|
from OCP.GeomConvert import GeomConvert
|
||||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
|
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
|
||||||
@ -34,6 +37,11 @@ def tmpdir(tmp_path_factory):
|
|||||||
return tmp_path_factory.mktemp("out")
|
return tmp_path_factory.mktemp("out")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def testdatadir():
|
||||||
|
return Path(__file__).parent.joinpath("testdata")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def box123():
|
def box123():
|
||||||
return Workplane().box(1, 2, 3)
|
return Workplane().box(1, 2, 3)
|
||||||
@ -59,6 +67,223 @@ def test_step_options(tmpdir):
|
|||||||
assert w.faces().size() == 6
|
assert w.faces().size() == 6
|
||||||
|
|
||||||
|
|
||||||
|
class TestDxfDocument(BaseTest):
|
||||||
|
"""Test class DxfDocument."""
|
||||||
|
|
||||||
|
def test_line(self):
|
||||||
|
workplane = Workplane().line(1, 1)
|
||||||
|
|
||||||
|
plane = workplane.plane
|
||||||
|
shape = toCompound(workplane).transformShape(plane.fG)
|
||||||
|
edges = shape.Edges()
|
||||||
|
|
||||||
|
result = DxfDocument._dxf_line(edges[0])
|
||||||
|
|
||||||
|
expected = ("LINE", {"start": (0.0, 0.0, 0.0), "end": (1.0, 1.0, 0.0)})
|
||||||
|
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_circle(self):
|
||||||
|
workplane = Workplane().circle(1)
|
||||||
|
|
||||||
|
plane = workplane.plane
|
||||||
|
shape = toCompound(workplane).transformShape(plane.fG)
|
||||||
|
edges = shape.Edges()
|
||||||
|
|
||||||
|
result = DxfDocument._dxf_circle(edges[0])
|
||||||
|
|
||||||
|
expected = ("CIRCLE", {"center": (0.0, 0.0, 0.0), "radius": 1.0})
|
||||||
|
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_arc(self):
|
||||||
|
workplane = Workplane().radiusArc((1, 1), 1)
|
||||||
|
|
||||||
|
plane = workplane.plane
|
||||||
|
shape = toCompound(workplane).transformShape(plane.fG)
|
||||||
|
edges = shape.Edges()
|
||||||
|
|
||||||
|
result_type, result_attributes = DxfDocument._dxf_circle(edges[0])
|
||||||
|
|
||||||
|
expected_type, expected_attributes = (
|
||||||
|
"ARC",
|
||||||
|
{"center": (1, 0, 0), "radius": 1, "start_angle": 90, "end_angle": 180,},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(expected_type, result_type)
|
||||||
|
self.assertTupleAlmostEquals(
|
||||||
|
expected_attributes["center"], result_attributes["center"], 3
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
expected_attributes["radius"], approx(result_attributes["radius"])
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
expected_attributes["start_angle"], result_attributes["start_angle"]
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
expected_attributes["end_angle"], result_attributes["end_angle"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ellipse(self):
|
||||||
|
workplane = Workplane().ellipse(2, 1, 0)
|
||||||
|
|
||||||
|
plane = workplane.plane
|
||||||
|
shape = toCompound(workplane).transformShape(plane.fG)
|
||||||
|
edges = shape.Edges()
|
||||||
|
|
||||||
|
result_type, result_attributes = DxfDocument._dxf_ellipse(edges[0])
|
||||||
|
|
||||||
|
expected_type, expected_attributes = (
|
||||||
|
"ELLIPSE",
|
||||||
|
{
|
||||||
|
"center": (0, 0, 0),
|
||||||
|
"major_axis": (2.0, 0, 0),
|
||||||
|
"ratio": 0.5,
|
||||||
|
"start_param": 0,
|
||||||
|
"end_param": 6.283185307179586,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(expected_type, result_type)
|
||||||
|
self.assertEqual(expected_attributes["center"], result_attributes["center"])
|
||||||
|
self.assertEqual(
|
||||||
|
expected_attributes["major_axis"], result_attributes["major_axis"]
|
||||||
|
)
|
||||||
|
self.assertEqual(expected_attributes["ratio"], result_attributes["ratio"])
|
||||||
|
self.assertEqual(
|
||||||
|
expected_attributes["start_param"], result_attributes["start_param"]
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
expected_attributes["end_param"], result_attributes["end_param"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_spline(self):
|
||||||
|
pts = [(0, 0), (0, 0.5), (1, 1)]
|
||||||
|
workplane = (
|
||||||
|
Workplane().spline(pts).close().extrude(1).edges("|Z").fillet(0.1).section()
|
||||||
|
)
|
||||||
|
|
||||||
|
plane = workplane.plane
|
||||||
|
shape = toCompound(workplane).transformShape(plane.fG)
|
||||||
|
edges = shape.Edges()
|
||||||
|
|
||||||
|
result_type, result_attributes = DxfDocument._dxf_spline(edges[0], plane)
|
||||||
|
|
||||||
|
expected_type, expected_attributes = (
|
||||||
|
"SPLINE",
|
||||||
|
{
|
||||||
|
"control_points": [
|
||||||
|
(-0.032010295564216654, 0.2020130195642037, 0.0),
|
||||||
|
(-0.078234124721739, 0.8475143728081896, 0.0),
|
||||||
|
(0.7171193004814275, 0.9728923786984539, 0.0),
|
||||||
|
],
|
||||||
|
"order": 3,
|
||||||
|
"knots": [
|
||||||
|
0.18222956891558767,
|
||||||
|
0.18222956891558767,
|
||||||
|
0.18222956891558767,
|
||||||
|
1.416096480384525,
|
||||||
|
1.416096480384525,
|
||||||
|
1.416096480384525,
|
||||||
|
],
|
||||||
|
"weights": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(expected_type, result_type)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
expected_attributes["control_points"], result_attributes["control_points"]
|
||||||
|
)
|
||||||
|
self.assertEqual(expected_attributes["order"], result_attributes["order"])
|
||||||
|
self.assertEqual(expected_attributes["knots"], result_attributes["knots"])
|
||||||
|
self.assertEqual(expected_attributes["weights"], result_attributes["weights"])
|
||||||
|
|
||||||
|
def test_add_layer_definition(self):
|
||||||
|
dxf = DxfDocument()
|
||||||
|
dxf.add_layer("layer_1")
|
||||||
|
|
||||||
|
self.assertIn("layer_1", dxf.document.layers)
|
||||||
|
|
||||||
|
def test_add_layer_definition_with_color(self):
|
||||||
|
dxf = DxfDocument()
|
||||||
|
dxf.add_layer("layer_1", color=2)
|
||||||
|
layer = dxf.document.layers.get("layer_1")
|
||||||
|
|
||||||
|
self.assertEqual(2, layer.color)
|
||||||
|
|
||||||
|
def test_add_layer_definition_with_linetype(self):
|
||||||
|
dxf = DxfDocument(setup=True)
|
||||||
|
dxf.add_layer("layer_1", linetype="CENTER")
|
||||||
|
layer = dxf.document.layers.get("layer_1")
|
||||||
|
|
||||||
|
self.assertEqual("CENTER", layer.dxf.linetype)
|
||||||
|
|
||||||
|
def test_add_shape_to_layer(self):
|
||||||
|
line = Workplane().line(0, 10)
|
||||||
|
|
||||||
|
dxf = DxfDocument(setup=True)
|
||||||
|
|
||||||
|
default_layer_names = set()
|
||||||
|
for layer in dxf.document.layers:
|
||||||
|
default_layer_names.add(layer.dxf.name)
|
||||||
|
|
||||||
|
dxf = dxf.add_layer("layer_1").add_shape(line, "layer_1")
|
||||||
|
|
||||||
|
expected_layer_names = default_layer_names.copy()
|
||||||
|
expected_layer_names.add("layer_1")
|
||||||
|
|
||||||
|
self.assertEqual({"0", "Defpoints"}, default_layer_names)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(dxf.msp))
|
||||||
|
self.assertEqual({"0", "Defpoints", "layer_1"}, expected_layer_names)
|
||||||
|
self.assertEqual("layer_1", dxf.msp[0].dxf.layer)
|
||||||
|
self.assertEqual("LINE", dxf.msp[0].dxftype())
|
||||||
|
|
||||||
|
def test_set_dxf_version(self):
|
||||||
|
dxfversion = "AC1032"
|
||||||
|
|
||||||
|
dxf_default = DxfDocument()
|
||||||
|
dxf = DxfDocument(dxfversion=dxfversion)
|
||||||
|
|
||||||
|
self.assertNotEqual(dxfversion, dxf_default.document.dxfversion)
|
||||||
|
self.assertEqual(dxfversion, dxf.document.dxfversion)
|
||||||
|
|
||||||
|
def test_set_units(self):
|
||||||
|
doc_units = 17
|
||||||
|
|
||||||
|
dxf_default = DxfDocument()
|
||||||
|
dxf = DxfDocument(doc_units=17)
|
||||||
|
|
||||||
|
self.assertNotEqual(doc_units, dxf_default.document.units)
|
||||||
|
self.assertEqual(doc_units, dxf.document.units)
|
||||||
|
|
||||||
|
def test_set_metadata(self):
|
||||||
|
metadata = {"CUSTOM_KEY": "custom value"}
|
||||||
|
|
||||||
|
dxf = DxfDocument(metadata=metadata)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
metadata["CUSTOM_KEY"], dxf.document.ezdxf_metadata().get("CUSTOM_KEY"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_add_shape_line(self):
|
||||||
|
workplane = Workplane().line(1, 1)
|
||||||
|
dxf = DxfDocument()
|
||||||
|
dxf.add_shape(workplane)
|
||||||
|
|
||||||
|
result = dxf.msp.query("LINE")[0]
|
||||||
|
|
||||||
|
expected = ezdxf.entities.line.Line.new(
|
||||||
|
dxfattribs={"start": (0.0, 0.0, 0.0), "end": (1.0, 1.0, 0.0),},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(expected.dxf.start, result.dxf.start)
|
||||||
|
self.assertEqual(expected.dxf.end, result.dxf.end)
|
||||||
|
|
||||||
|
def test_DxfDocument_import(self):
|
||||||
|
assert isinstance(exporters.DxfDocument(), DxfDocument)
|
||||||
|
|
||||||
|
|
||||||
class TestExporters(BaseTest):
|
class TestExporters(BaseTest):
|
||||||
def _exportBox(self, eType, stringsToFind, tolerance=0.1, angularTolerance=0.1):
|
def _exportBox(self, eType, stringsToFind, tolerance=0.1, angularTolerance=0.1):
|
||||||
"""
|
"""
|
||||||
@ -420,3 +645,26 @@ def test_dxf_approx():
|
|||||||
assert _check_dxf_no_spline("limit2.dxf")
|
assert _check_dxf_no_spline("limit2.dxf")
|
||||||
|
|
||||||
assert w1.val().Area() == approx(w1_i2.val().Area(), 1e-3)
|
assert w1.val().Area() == approx(w1_i2.val().Area(), 1e-3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dxf_text(tmpdir, testdatadir):
|
||||||
|
|
||||||
|
w1 = (
|
||||||
|
Workplane("XZ")
|
||||||
|
.box(8, 8, 1)
|
||||||
|
.faces("<Y")
|
||||||
|
.workplane()
|
||||||
|
.text(
|
||||||
|
",,", 10, -1, True, fontPath=str(Path(testdatadir, "OpenSans-Regular.ttf")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fname = tmpdir.joinpath(f"dxf_text.dxf").resolve()
|
||||||
|
exporters.exportDXF(w1.section(), fname)
|
||||||
|
|
||||||
|
s2 = Sketch().importDXF(fname)
|
||||||
|
w2 = Workplane("XZ", origin=(0, -0.5, 0)).placeSketch(s2).extrude(-1)
|
||||||
|
|
||||||
|
assert w1.val().Volume() == approx(59.983287, 1e-2)
|
||||||
|
assert w2.val().Volume() == approx(w1.val().Volume(), 1e-2)
|
||||||
|
assert w2.intersect(w1).val().Volume() == approx(w1.val().Volume(), 1e-2)
|
||||||
|
Reference in New Issue
Block a user