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 .amf import AmfWriter
|
||||
from .threemf import ThreeMFWriter
|
||||
from .dxf import exportDXF
|
||||
from .dxf import exportDXF, DxfDocument
|
||||
from .vtk import exportVTP
|
||||
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 ..shapes import Edge
|
||||
from .utils import toCompound
|
||||
|
||||
from OCP.gp import gp_Dir
|
||||
from OCP.GeomConvert import GeomConvert
|
||||
|
||||
from typing import Optional, Literal
|
||||
|
||||
import ezdxf
|
||||
|
||||
CURVE_TOLERANCE = 1e-9
|
||||
ApproxOptions = Literal["spline", "arc"]
|
||||
DxfEntityAttributes = Tuple[
|
||||
Literal["ARC", "CIRCLE", "ELLIPSE", "LINE", "SPLINE",], Dict[str, Any]
|
||||
]
|
||||
|
||||
|
||||
def _dxf_line(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
|
||||
class DxfDocument:
|
||||
"""Create DXF document from CadQuery objects.
|
||||
|
||||
msp.add_line(
|
||||
e.startPoint().toTuple(), e.endPoint().toTuple(),
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
r = circ.Radius()
|
||||
c = circ.Location()
|
||||
radius = circ.Radius()
|
||||
location = circ.Location()
|
||||
|
||||
c_dy = circ.YAxis().Direction()
|
||||
c_dz = circ.Axis().Direction()
|
||||
direction_y = circ.YAxis().Direction()
|
||||
direction_z = circ.Axis().Direction()
|
||||
|
||||
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)
|
||||
a2 = RAD2DEG * (geom.LastParameter() - phi)
|
||||
else:
|
||||
a1 = -RAD2DEG * (geom.LastParameter() - phi) + 180
|
||||
a2 = -RAD2DEG * (geom.FirstParameter() - phi) + 180
|
||||
|
||||
if e.IsClosed():
|
||||
msp.add_circle((c.X(), c.Y(), c.Z()), r)
|
||||
if edge.IsClosed():
|
||||
return (
|
||||
"CIRCLE",
|
||||
{
|
||||
"center": (location.X(), location.Y(), location.Z()),
|
||||
"radius": radius,
|
||||
},
|
||||
)
|
||||
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()
|
||||
|
||||
r1 = ellipse.MinorRadius()
|
||||
@ -60,26 +250,38 @@ def _dxf_ellipse(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
|
||||
xdir = ellipse.XAxis().Direction()
|
||||
xax = r2 * xdir.XYZ()
|
||||
|
||||
msp.add_ellipse(
|
||||
(c.X(), c.Y(), c.Z()),
|
||||
(xax.X(), xax.Y(), xax.Z()),
|
||||
r1 / r2,
|
||||
geom.FirstParameter(),
|
||||
geom.LastParameter(),
|
||||
return (
|
||||
"ELLIPSE",
|
||||
{
|
||||
"center": (c.X(), c.Y(), c.Z()),
|
||||
"major_axis": (xax.X(), xax.Y(), xax.Z()),
|
||||
"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())
|
||||
|
||||
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
|
||||
spline.Transform(plane.fG.wrapped.Trsf())
|
||||
spline.Transform(adaptor.Trsf())
|
||||
|
||||
order = spline.Degree() + 1
|
||||
knots = list(spline.KnotSequence())
|
||||
@ -94,25 +296,25 @@ def _dxf_spline(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
|
||||
pad = spline.NbKnots() - spline.LastUKnotIndex()
|
||||
poles += poles[:pad]
|
||||
|
||||
dxf_spline = ezdxf.math.BSpline(poles, order, knots, weights)
|
||||
|
||||
msp.add_spline().apply_construction_tool(dxf_spline)
|
||||
|
||||
|
||||
DXF_CONVERTERS = {
|
||||
"LINE": _dxf_line,
|
||||
"CIRCLE": _dxf_circle,
|
||||
"ELLIPSE": _dxf_ellipse,
|
||||
"BSPLINE": _dxf_spline,
|
||||
}
|
||||
return (
|
||||
"SPLINE",
|
||||
{
|
||||
"control_points": poles,
|
||||
"order": order,
|
||||
"knots": knots,
|
||||
"weights": weights,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def exportDXF(
|
||||
w: Workplane,
|
||||
fname: str,
|
||||
approx: Optional[Literal["spline", "arc"]] = None,
|
||||
approx: Optional[ApproxOptions] = None,
|
||||
tolerance: float = 1e-3,
|
||||
):
|
||||
*,
|
||||
doc_units: int = units.MM,
|
||||
) -> None:
|
||||
"""
|
||||
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
|
||||
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``).
|
||||
"""
|
||||
|
||||
plane = w.plane
|
||||
shape = toCompound(w).transformShape(plane.fG)
|
||||
|
||||
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)
|
||||
dxf = DxfDocument(approx=approx, tolerance=tolerance, doc_units=doc_units)
|
||||
dxf.add_shape(w)
|
||||
dxf.document.saveas(fname)
|
||||
|
@ -206,6 +206,7 @@ File Management and Export
|
||||
importers.importStep
|
||||
importers.importDXF
|
||||
exporters.export
|
||||
occ_impl.exporters.dxf.DxfDocument
|
||||
|
||||
|
||||
Iteration Methods
|
||||
|
@ -101,3 +101,8 @@ Class Details
|
||||
:members:
|
||||
|
||||
.. 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.viewcode",
|
||||
"sphinx.ext.autosummary",
|
||||
"sphinx.ext.intersphinx",
|
||||
"cadquery.cq_directive",
|
||||
"sphinx.ext.mathjax",
|
||||
"sphinx_autodoc_multimethod",
|
||||
@ -212,6 +213,11 @@ html_show_sphinx = False
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = "CadQuerydoc"
|
||||
|
||||
# -- Options for intersphinx --------------------------------------------------
|
||||
|
||||
intersphinx_mapping = {
|
||||
"ezdxf-stable": ("https://ezdxf.readthedocs.io/en/stable/", None),
|
||||
}
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
|
@ -239,17 +239,99 @@ optimum values that will produce an acceptable mesh.
|
||||
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:
|
||||
|
||||
* ``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
|
||||
|
||||
:caption: DXF document with curves approximated with cubic splines.
|
||||
|
||||
cq.exporters.exportDXF(
|
||||
result,
|
||||
'/path/to/file/object.dxf',
|
||||
"/path/to/file/object.dxf",
|
||||
approx="spline"
|
||||
)
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Tests basic workplane functionality
|
||||
Tests exporters
|
||||
"""
|
||||
# core modules
|
||||
import os
|
||||
@ -16,6 +16,7 @@ from pytest import approx
|
||||
from cadquery import (
|
||||
exporters,
|
||||
importers,
|
||||
Sketch,
|
||||
Workplane,
|
||||
Edge,
|
||||
Vertex,
|
||||
@ -24,6 +25,8 @@ from cadquery import (
|
||||
Location,
|
||||
Vector,
|
||||
)
|
||||
from cadquery.occ_impl.exporters.dxf import DxfDocument
|
||||
from cadquery.occ_impl.exporters.utils import toCompound
|
||||
from tests import BaseTest
|
||||
from OCP.GeomConvert import GeomConvert
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
|
||||
@ -34,6 +37,11 @@ def tmpdir(tmp_path_factory):
|
||||
return tmp_path_factory.mktemp("out")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def testdatadir():
|
||||
return Path(__file__).parent.joinpath("testdata")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def box123():
|
||||
return Workplane().box(1, 2, 3)
|
||||
@ -59,6 +67,223 @@ def test_step_options(tmpdir):
|
||||
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):
|
||||
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 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