Skip to content

Commit 59543da

Browse files
Merge 0b99e28 into 6c8030d
2 parents 6c8030d + 0b99e28 commit 59543da

File tree

6 files changed

+163
-15
lines changed

6 files changed

+163
-15
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ target/*
1010
MANIFEST
1111
out.*
1212
res?.dxf
13+
limit?.dxf
1314
.~*
1415
.*.swp
1516
assy.wrl

cadquery/occ_impl/exporters/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import io as StringIO
44

5-
from typing import IO, Optional, Union, cast
5+
from typing import IO, Optional, Union, cast, Dict, Any
66
from typing_extensions import Literal
77

88
from OCP.VrmlAPI import VrmlAPI
@@ -43,7 +43,7 @@ def export(
4343
exportType: Optional[ExportLiterals] = None,
4444
tolerance: float = 0.1,
4545
angularTolerance: float = 0.1,
46-
opt=None,
46+
opt: Optional[Dict[str, Any]] = None,
4747
):
4848

4949
"""
@@ -60,6 +60,9 @@ def export(
6060
shape: Shape
6161
f: IO
6262

63+
if not opt:
64+
opt = {}
65+
6366
if isinstance(w, Workplane):
6467
shape = toCompound(w)
6568
else:
@@ -98,21 +101,18 @@ def export(
98101
aw.writeAmf(f)
99102

100103
elif exportType == ExportTypes.THREEMF:
101-
tmfw = ThreeMFWriter(shape, tolerance, angularTolerance, **opt or {})
104+
tmfw = ThreeMFWriter(shape, tolerance, angularTolerance, **opt)
102105
with open(fname, "wb") as f:
103106
tmfw.write3mf(f)
104107

105108
elif exportType == ExportTypes.DXF:
106109
if isinstance(w, Workplane):
107-
exportDXF(w, fname)
110+
exportDXF(w, fname, **opt)
108111
else:
109112
raise ValueError("Only Workplanes can be exported as DXF")
110113

111114
elif exportType == ExportTypes.STEP:
112-
if opt:
113-
shape.exportStep(fname, **opt)
114-
else:
115-
shape.exportStep(fname)
115+
shape.exportStep(fname, **opt)
116116

117117
elif exportType == ExportTypes.STL:
118118
if opt:

cadquery/occ_impl/exporters/dxf.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
from ...cq import Workplane, Plane
1+
from ...cq import Workplane, Plane, Face
22
from ...units import RAD2DEG
33
from ..shapes import Edge
44
from .utils import toCompound
55

66
from OCP.gp import gp_Dir
77
from OCP.GeomConvert import GeomConvert
88

9+
from typing import Optional, Literal
10+
911
import ezdxf
1012

1113
CURVE_TOLERANCE = 1e-9
1214

1315

14-
def _dxf_line(e, msp, plane):
16+
def _dxf_line(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
1517

1618
msp.add_line(
1719
e.startPoint().toTuple(), e.endPoint().toTuple(),
@@ -105,12 +107,21 @@ def _dxf_spline(e: Edge, msp: ezdxf.layouts.Modelspace, plane: Plane):
105107
}
106108

107109

108-
def exportDXF(w: Workplane, fname: str):
110+
def exportDXF(
111+
w: Workplane,
112+
fname: str,
113+
approx: Optional[Literal["spline", "arc"]] = None,
114+
tolerance: float = 1e-3,
115+
):
109116
"""
110117
Export Workplane content to DXF. Works with 2D sections.
111118
112119
:param w: Workplane to be exported.
113-
:param fname: output filename.
120+
:param fname: Output filename.
121+
:param approx: Approximation strategy. None means no approximation is applied.
122+
"spline" results in all splines being approximated as cubic splines. "arc" results
123+
in all curves being approximated as arcs and straight segments.
124+
:param tolerance: Approximation tolerance.
114125
115126
"""
116127

@@ -120,7 +131,22 @@ def exportDXF(w: Workplane, fname: str):
120131
dxf = ezdxf.new()
121132
msp = dxf.modelspace()
122133

123-
for e in shape.Edges():
134+
if approx == "spline":
135+
edges = [
136+
e.toSplines() if e.geomType() == "BSPLINE" else e for e in shape.Edges()
137+
]
138+
139+
elif approx == "arc":
140+
edges = []
141+
142+
# this is needed to handle free wires
143+
for el in shape.Wires():
144+
edges.extend(Face.makeFromWires(el).toArcs(tolerance).Edges())
145+
146+
else:
147+
edges = shape.Edges()
148+
149+
for e in edges:
124150

125151
conv = DXF_CONVERTERS.get(e.geomType(), _dxf_spline)
126152
conv(e, msp, plane)

cadquery/occ_impl/shapes.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@
240240

241241
from OCP.Interface import Interface_Static
242242

243+
from OCP.ShapeCustom import ShapeCustom, ShapeCustom_RestrictionParameters
244+
245+
from OCP.BRepAlgo import BRepAlgo
246+
243247
from math import pi, sqrt, inf
244248

245249
import warnings
@@ -1252,6 +1256,34 @@ def tessellate(
12521256

12531257
return vertices, triangles
12541258

1259+
def toSplines(
1260+
self: T, degree: int = 3, tolerance: float = 1e-3, nurbs: bool = False
1261+
) -> T:
1262+
"""
1263+
Approximate shape with b-splines of the specified degree.
1264+
1265+
:param degree: Maximum degree.
1266+
:param tolerance: Approximation tolerance.
1267+
:param nurbs: Use rational splines.
1268+
"""
1269+
1270+
params = ShapeCustom_RestrictionParameters()
1271+
1272+
result = ShapeCustom.BSplineRestriction_s(
1273+
self.wrapped,
1274+
tolerance, # 3D tolerance
1275+
tolerance, # 2D tolerance
1276+
degree,
1277+
1, # dumy value, degree is leading
1278+
ga.GeomAbs_C0,
1279+
ga.GeomAbs_C0,
1280+
True, # set degree to be leading
1281+
not nurbs,
1282+
params,
1283+
)
1284+
1285+
return self.__class__(result)
1286+
12551287
def toVtkPolyData(
12561288
self,
12571289
tolerance: Optional[float] = None,
@@ -2562,6 +2594,15 @@ def project(self, other: "Face", d: VectorLike) -> "Face":
25622594

25632595
return self.constructOn(other, outer_p, *inner_p)
25642596

2597+
def toArcs(self, tolerance: float = 1e-3) -> "Face":
2598+
"""
2599+
Approximate planar face with arcs and straight line segments.
2600+
2601+
:param tolerance: Approximation tolerance.
2602+
"""
2603+
2604+
return self.__class__(BRepAlgo.ConvertFace_s(self.wrapped, tolerance))
2605+
25652606

25662607
class Shell(Shape):
25672608
"""

doc/importexport.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,24 @@ optimum values that will produce an acceptable mesh.
231231
232232
exporters.export(result, '/path/to/file/mesh.vrml', tolerance=0.01, angularTolerance=0.1)
233233
234+
Exporting DXF
235+
##############
236+
237+
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:
238+
239+
* ``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.
240+
* ``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).
241+
242+
.. code-block:: python
243+
244+
245+
cq.exporters.exportDXF(
246+
result,
247+
'/path/to/file/object.dxf',
248+
approx="spline"
249+
)
250+
251+
234252
Exporting Other Formats
235253
########################
236254

tests/test_exporters.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,22 @@
88
import re
99
import sys
1010
import pytest
11+
import ezdxf
12+
13+
from pytest import approx
1114

1215
# my modules
13-
from cadquery import *
14-
from cadquery import exporters, importers
16+
from cadquery import (
17+
exporters,
18+
importers,
19+
Workplane,
20+
Edge,
21+
Vertex,
22+
Assembly,
23+
Plane,
24+
Location,
25+
Vector,
26+
)
1527
from tests import BaseTest
1628
from OCP.GeomConvert import GeomConvert
1729
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
@@ -357,3 +369,53 @@ def test_tessellate(box123):
357369
verts, triangles = box123.val().tessellate(1e-6)
358370
assert len(verts) == 24
359371
assert len(triangles) == 12
372+
373+
374+
def _dxf_spline_max_degree(fname):
375+
376+
dxf = ezdxf.readfile(fname)
377+
msp = dxf.modelspace()
378+
379+
rv = 0
380+
381+
for el in msp:
382+
if isinstance(el, ezdxf.entities.Spline):
383+
rv = el.dxf.degree if el.dxf.degree > rv else rv
384+
385+
return rv
386+
387+
388+
def _check_dxf_no_spline(fname):
389+
390+
dxf = ezdxf.readfile(fname)
391+
msp = dxf.modelspace()
392+
393+
for el in msp:
394+
if isinstance(el, ezdxf.entities.Spline):
395+
return False
396+
397+
return True
398+
399+
400+
def test_dxf_approx():
401+
402+
pts = [(0, 0), (0, 0.5), (1, 1)]
403+
w1 = Workplane().spline(pts).close().extrude(1).edges("|Z").fillet(0.1).section()
404+
exporters.exportDXF(w1, "orig.dxf")
405+
406+
assert _dxf_spline_max_degree("orig.dxf") == 6
407+
408+
exporters.exportDXF(w1, "limit1.dxf", approx="spline")
409+
w1_i1 = importers.importDXF("limit1.dxf")
410+
411+
assert _dxf_spline_max_degree("limit1.dxf") == 3
412+
413+
assert w1.val().Area() == approx(w1_i1.val().Area(), 1e-3)
414+
assert w1.edges().size() == w1_i1.edges().size()
415+
416+
exporters.exportDXF(w1, "limit2.dxf", approx="arc")
417+
w1_i2 = importers.importDXF("limit2.dxf")
418+
419+
assert _check_dxf_no_spline("limit2.dxf")
420+
421+
assert w1.val().Area() == approx(w1_i2.val().Area(), 1e-3)

0 commit comments

Comments
 (0)