Skip to content

Commit 6130774

Browse files
committed
DXF multilayer support
Also supports: * setting units * setting color by layer * setting line type by layer
1 parent 2997a3a commit 6130774

File tree

1 file changed

+253
-103
lines changed
  • cadquery/occ_impl/exporters

1 file changed

+253
-103
lines changed

cadquery/occ_impl/exporters/dxf.py

Lines changed: 253 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,269 @@
1-
from ...cq import Workplane, Plane
2-
from ...units import RAD2DEG
3-
from ..shapes import Edge
4-
from .utils import toCompound
5-
6-
from OCP.gp import gp_Dir
7-
from OCP.GeomConvert import GeomConvert
1+
from typing import Any, Union
82

93
import ezdxf
4+
from ezdxf import units, zoom
5+
from ezdxf.entities import factory
6+
from OCP.GeomConvert import GeomConvert
7+
from OCP.gp import gp_Dir
108

11-
CURVE_TOLERANCE = 1e-9
12-
13-
14-
def _dxf_line(e, msp, plane):
15-
16-
msp.add_line(
17-
e.startPoint().toTuple(), e.endPoint().toTuple(),
18-
)
19-
20-
21-
def _dxf_circle(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
22-
23-
geom = e._geomAdaptor()
24-
circ = geom.Circle()
25-
26-
r = circ.Radius()
27-
c = circ.Location()
28-
29-
c_dy = circ.YAxis().Direction()
30-
c_dz = circ.Axis().Direction()
31-
32-
dy = gp_Dir(0, 1, 0)
33-
34-
phi = c_dy.AngleWithRef(dy, c_dz)
35-
36-
if c_dz.XYZ().Z() > 0:
37-
a1 = RAD2DEG * (geom.FirstParameter() - phi)
38-
a2 = RAD2DEG * (geom.LastParameter() - phi)
39-
else:
40-
a1 = -RAD2DEG * (geom.LastParameter() - phi) + 180
41-
a2 = -RAD2DEG * (geom.FirstParameter() - phi) + 180
42-
43-
if e.IsClosed():
44-
msp.add_circle((c.X(), c.Y(), c.Z()), r)
45-
else:
46-
msp.add_arc((c.X(), c.Y(), c.Z()), r, a1, a2)
47-
48-
49-
def _dxf_ellipse(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
50-
51-
geom = e._geomAdaptor()
52-
ellipse = geom.Ellipse()
53-
54-
r1 = ellipse.MinorRadius()
55-
r2 = ellipse.MajorRadius()
9+
from ...cq import Plane, Workplane
10+
from ...units import RAD2DEG
11+
from ..shapes import Edge
12+
from .utils import toCompound
5613

57-
c = ellipse.Location()
58-
xdir = ellipse.XAxis().Direction()
59-
xax = r2 * xdir.XYZ()
6014

61-
msp.add_ellipse(
62-
(c.X(), c.Y(), c.Z()),
63-
(xax.X(), xax.Y(), xax.Z()),
64-
r1 / r2,
65-
geom.FirstParameter(),
66-
geom.LastParameter(),
67-
)
15+
class DxfDocument:
16+
"""Create DXF document from CadQuery objects.
6817
18+
DXF exporter utilising `ezdxf <https://ezdxf.readthedocs.io/>`_.
6919
70-
def _dxf_spline(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
20+
Example usage
7121
72-
adaptor = e._geomAdaptor()
73-
curve = GeomConvert.CurveToBSplineCurve_s(adaptor.Curve().Curve())
22+
Single layer DXF document:
7423
75-
spline = GeomConvert.SplitBSplineCurve_s(
76-
curve, adaptor.FirstParameter(), adaptor.LastParameter(), CURVE_TOLERANCE
77-
)
24+
.. code-block:: python
7825
79-
# need to apply the transform on the geometry level
80-
spline.Transform(plane.fG.wrapped.Trsf())
26+
rectangle = cq.Workplane().rect(10, 20)
8127
82-
order = spline.Degree() + 1
83-
knots = list(spline.KnotSequence())
84-
poles = [(p.X(), p.Y(), p.Z()) for p in spline.Poles()]
85-
weights = (
86-
[spline.Weight(i) for i in range(1, spline.NbPoles() + 1)]
87-
if spline.IsRational()
88-
else None
89-
)
28+
dxf = DxfDocument()
29+
dxf.add_shape(rectangle)
30+
dxf.document.saveas("rectangle.dxf")
9031
91-
if spline.IsPeriodic():
92-
pad = spline.NbKnots() - spline.LastUKnotIndex()
93-
poles += poles[:pad]
32+
Multilayer DXF document:
9433
95-
dxf_spline = ezdxf.math.BSpline(poles, order, knots, weights)
34+
.. code-block:: python
9635
97-
msp.add_spline().apply_construction_tool(dxf_spline)
36+
rectangle = cq.Workplane().rect(10, 20)
37+
circle = cq.Workplane().circle(3)
9838
39+
dxf = DxfDocument()
40+
dxf = (
41+
dxf.add_layer("layer_1", color=2)
42+
.add_layer("layer_2", color=3)
43+
.add_shape(rectangle, "layer_1")
44+
.add_shape(circle, "layer_2")
45+
)
46+
dxf.document.saveas("rectangle-with-hole.dxf")
47+
"""
9948

100-
DXF_CONVERTERS = {
101-
"LINE": _dxf_line,
102-
"CIRCLE": _dxf_circle,
103-
"ELLIPSE": _dxf_ellipse,
104-
"BSPLINE": _dxf_spline,
105-
}
49+
CURVE_TOLERANCE = 1e-9
50+
51+
def __init__(
52+
self,
53+
dxfversion: str = "AC1027",
54+
setup: Union[bool, list[str]] = False,
55+
doc_units: int = units.MM,
56+
*,
57+
metadata: Union[dict[str, str], None] = None,
58+
) -> None:
59+
"""Initialise DXF document.
60+
61+
:param dxfversion: DXF version specifier as string, default is "AC1027"
62+
respectively "R2013"
63+
:param setup: setup default styles, ``False`` for no setup, ``True`` to setup
64+
everything or a list of topics as strings, e.g. ["linetypes", "styles"]
65+
:param doc_units: ezdxf document/modelspace units ``ezdxf.enums.InsertUnits``
66+
:param metadata: document metadata a dictionary of name value pairs
67+
"""
68+
if metadata is None:
69+
metadata = {}
70+
71+
self._DISPATCH_MAP = {
72+
"LINE": self._dxf_line,
73+
"CIRCLE": self._dxf_circle,
74+
"ELLIPSE": self._dxf_ellipse,
75+
}
76+
77+
self.document = ezdxf.new(dxfversion=dxfversion, setup=setup, units=doc_units) # type: ignore[attr-defined]
78+
self.msp = self.document.modelspace()
79+
80+
doc_metadata = self.document.ezdxf_metadata()
81+
for key, value in metadata.items():
82+
doc_metadata[key] = value
83+
84+
def add_layer(
85+
self, name: str, *, color: int = 1, linetype: str = "Continuous"
86+
) -> "DxfDocument":
87+
"""Add layer to DXF document.
88+
89+
:param name: ezdxf document layer name
90+
:param color: ezdxf color
91+
:param linetype: ezdxf line type
92+
93+
:return: DxfDocument
94+
"""
95+
self.document.layers.add(name, color=color, linetype=linetype)
96+
97+
return self
98+
99+
def add_shape(self, workplane: Workplane, layer: str = "") -> "DxfDocument":
100+
"""Add CadQuery shape to a DXF layer.
101+
102+
:param workplane: CadQuery Workplane
103+
:param layer: ezdxf document layer name
104+
105+
:return: DxfDocument
106+
"""
107+
plane = workplane.plane
108+
shape = toCompound(workplane).transformShape(plane.fG)
109+
110+
general_attributes = {}
111+
if layer:
112+
general_attributes["layer"] = layer
113+
114+
for edge in shape.Edges():
115+
converter = self._DISPATCH_MAP.get(edge.geomType(), None)
116+
117+
if converter:
118+
entity_type, entity_attributes = converter(edge)
119+
entity = factory.new(
120+
entity_type, dxfattribs=entity_attributes | general_attributes
121+
)
122+
self.msp.add_entity(entity)
123+
else:
124+
_, entity_attributes = self._dxf_spline(edge, plane)
125+
entity = ezdxf.math.BSpline(**entity_attributes)
126+
self.msp.add_spline(
127+
dxfattribs=general_attributes
128+
).apply_construction_tool(entity)
129+
130+
zoom.extents(self.msp)
131+
132+
return self
133+
134+
@staticmethod
135+
def _dxf_line(edge: Edge) -> tuple[str, dict[str, Any]]:
136+
"""Convert a Line to DXF attributes.
137+
138+
:param edge: CadQuery Edge to be converted to a DXF line
139+
140+
:return: dictionary of DXF entity attributes for creating a line
141+
"""
142+
return (
143+
"LINE",
144+
{"start": edge.startPoint().toTuple(), "end": edge.endPoint().toTuple(),},
145+
)
146+
147+
@staticmethod
148+
def _dxf_circle(edge: Edge) -> tuple[str, dict[str, Any]]:
149+
"""Convert a Circle to DXF attributes.
150+
151+
:param edge: CadQuery Edge to be converted to a DXF circle
152+
153+
:return: dictionary of DXF entity attributes for creating either a circle or arc
154+
"""
155+
geom = edge._geomAdaptor()
156+
circ = geom.Circle()
157+
158+
radius = circ.Radius()
159+
location = circ.Location()
160+
161+
direction_y = circ.YAxis().Direction()
162+
direction_z = circ.Axis().Direction()
163+
164+
dy = gp_Dir(0, 1, 0)
165+
166+
phi = direction_y.AngleWithRef(dy, direction_z)
167+
168+
if direction_z.XYZ().Z() > 0:
169+
a1 = RAD2DEG * (geom.FirstParameter() - phi)
170+
a2 = RAD2DEG * (geom.LastParameter() - phi)
171+
else:
172+
a1 = -RAD2DEG * (geom.LastParameter() - phi) + 180
173+
a2 = -RAD2DEG * (geom.FirstParameter() - phi) + 180
174+
175+
if edge.IsClosed():
176+
return (
177+
"CIRCLE",
178+
{
179+
"center": (location.X(), location.Y(), location.Z()),
180+
"radius": radius,
181+
},
182+
)
183+
else:
184+
return (
185+
"ARC",
186+
{
187+
"center": (location.X(), location.Y(), location.Z()),
188+
"radius": radius,
189+
"start_angle": a1,
190+
"end_angle": a2,
191+
},
192+
)
193+
194+
@staticmethod
195+
def _dxf_ellipse(edge: Edge) -> tuple[str, dict[str, Any]]:
196+
"""Convert an Ellipse to DXF attributes.
197+
198+
:param edge: CadQuery Edge to be converted to a DXF ellipse
199+
200+
:return: dictionary of DXF entity attributes for creating an ellipse
201+
"""
202+
geom = edge._geomAdaptor()
203+
ellipse = geom.Ellipse()
204+
205+
r1 = ellipse.MinorRadius()
206+
r2 = ellipse.MajorRadius()
207+
208+
c = ellipse.Location()
209+
xdir = ellipse.XAxis().Direction()
210+
xax = r2 * xdir.XYZ()
211+
212+
return (
213+
"ELLIPSE",
214+
{
215+
"center": (c.X(), c.Y(), c.Z()),
216+
"major_axis": (xax.X(), xax.Y(), xax.Z()),
217+
"ratio": r1 / r2,
218+
"start_param": geom.FirstParameter(),
219+
"end_param": geom.LastParameter(),
220+
},
221+
)
222+
223+
@classmethod
224+
def _dxf_spline(cls, edge: Edge, plane: Plane) -> tuple[str, dict[str, Any]]:
225+
"""Convert a Spline to ezdxf.math.BSpline parameters.
226+
227+
:param edge: CadQuery Edge to be converted to a DXF spline
228+
:param plane: CadQuery Plane
229+
230+
:return: dictionary of ezdxf.math.BSpline parameters
231+
"""
232+
adaptor = edge._geomAdaptor()
233+
curve = GeomConvert.CurveToBSplineCurve_s(adaptor.Curve().Curve())
234+
235+
spline = GeomConvert.SplitBSplineCurve_s(
236+
curve,
237+
adaptor.FirstParameter(),
238+
adaptor.LastParameter(),
239+
cls.CURVE_TOLERANCE,
240+
)
241+
242+
# need to apply the transform on the geometry level
243+
spline.Transform(plane.fG.wrapped.Trsf())
244+
245+
order = spline.Degree() + 1
246+
knots = list(spline.KnotSequence())
247+
poles = [(p.X(), p.Y(), p.Z()) for p in spline.Poles()]
248+
weights = (
249+
[spline.Weight(i) for i in range(1, spline.NbPoles() + 1)]
250+
if spline.IsRational()
251+
else None
252+
)
253+
254+
if spline.IsPeriodic():
255+
pad = spline.NbKnots() - spline.LastUKnotIndex()
256+
poles += poles[:pad]
257+
258+
return (
259+
"SPLINE",
260+
{
261+
"control_points": poles,
262+
"order": order,
263+
"knots": knots,
264+
"weights": weights,
265+
},
266+
)
106267

107268

108269
def exportDXF(w: Workplane, fname: str):
@@ -111,18 +272,7 @@ def exportDXF(w: Workplane, fname: str):
111272
112273
:param w: Workplane to be exported.
113274
:param fname: output filename.
114-
115275
"""
116-
117-
plane = w.plane
118-
shape = toCompound(w).transformShape(plane.fG)
119-
120-
dxf = ezdxf.new()
121-
msp = dxf.modelspace()
122-
123-
for e in shape.Edges():
124-
125-
conv = DXF_CONVERTERS.get(e.geomType(), _dxf_spline)
126-
conv(e, msp, plane)
127-
128-
dxf.saveas(fname)
276+
dxf = DxfDocument()
277+
dxf.add_shape(w)
278+
dxf.document.saveas(fname)

0 commit comments

Comments
 (0)