Skip to content

Commit 03fad35

Browse files
authored
Merge pull request #415 from CadQuery/dxf-export
DXF export (2D) and exporters cleanup
2 parents 446cff7 + 62746e3 commit 03fad35

File tree

14 files changed

+612
-347
lines changed

14 files changed

+612
-347
lines changed

cadquery/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
StringSyntaxSelector,
2828
Selector,
2929
)
30-
from .cq import CQ, Workplane, selectors
30+
from .cq import CQ, Workplane
31+
from . import selectors
3132
from . import plugins
3233

3334

cadquery/cq.py

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,30 @@
3434
)
3535
from typing_extensions import Literal
3636

37-
from . import (
38-
Vector,
39-
Plane,
40-
Matrix,
41-
Location,
37+
38+
from .occ_impl.geom import Vector, Plane, Location
39+
from .occ_impl.shapes import (
4240
Shape,
4341
Edge,
4442
Wire,
4543
Face,
4644
Solid,
4745
Compound,
4846
sortWiresByBuildOrder,
49-
selectors,
50-
exporters,
5147
)
5248

49+
from .occ_impl.exporters.svg import getSVG, exportSVG
50+
5351
from .utils import deprecate_kwarg, deprecate
5452

53+
from .selectors import (
54+
Selector,
55+
PerpendicularDirSelector,
56+
NearestToPointSelector,
57+
StringSyntaxSelector,
58+
)
59+
5560
CQObject = Union[Vector, Location, Shape]
56-
Selector = selectors.Selector
5761
VectorLike = Union[Tuple[float, float], Tuple[float, float, float], Vector]
5862

5963

@@ -180,42 +184,6 @@ def __init__(
180184
self.ctx = CQContext()
181185
self._tag = None
182186

183-
'''
184-
def __init__(self, obj: Optional[CQObject]) -> None:
185-
"""
186-
Construct a new CadQuery (CQ) object that wraps a CAD primitive.
187-
188-
:param obj: Object to Wrap.
189-
:type obj: A CAD Primitive ( wire,vertex,face,solid,edge )
190-
"""
191-
self.objects = []
192-
self.ctx = CQContext()
193-
self.parent = None
194-
self._tag = None
195-
196-
if obj: # guarded because sometimes None for internal use
197-
self.objects.append(obj)
198-
199-
def newObject(self, objlist: Sequence[CQObject]) -> "Workplane":
200-
"""
201-
Make a new CQ object.
202-
203-
:param objlist: The stack of objects to use
204-
:type objlist: a list of CAD primitives ( wire,face,edge,solid,vertex,etc )
205-
206-
The parent of the new object will be set to the current object,
207-
to preserve the chain correctly.
208-
209-
Custom plugins and subclasses should use this method to create new CQ objects
210-
correctly.
211-
"""
212-
r = Workplane() # create a completely blank one
213-
r.parent = self
214-
r.ctx = self.ctx # context solid remains the same
215-
r.objects = list(objlist)
216-
return r
217-
'''
218-
219187
def tag(self, name: str) -> "Workplane":
220188
"""
221189
Tags the current CQ object for later reference.
@@ -753,7 +721,7 @@ def _selectObjects(
753721
selectorObj: Selector
754722
if selector:
755723
if isinstance(selector, str):
756-
selectorObj = selectors.StringSyntaxSelector(selector)
724+
selectorObj = StringSyntaxSelector(selector)
757725
else:
758726
selectorObj = selector
759727
toReturn = selectorObj.filter(toReturn)
@@ -979,7 +947,7 @@ def toSvg(self, opts: Any = None) -> str:
979947
:type opts: dictionary, width and height
980948
:return: a string that contains SVG that represents this item.
981949
"""
982-
return exporters.getSVG(self.val(), opts)
950+
return getSVG(self.val(), opts)
983951

984952
def exportSvg(self, fileName: str) -> None:
985953
"""
@@ -990,7 +958,7 @@ def exportSvg(self, fileName: str) -> None:
990958
:param fileName: the filename to export
991959
:type fileName: String, absolute path to the file
992960
"""
993-
exporters.exportSVG(self, fileName)
961+
exportSVG(self, fileName)
994962

995963
def rotateAboutCenter(
996964
self, axisEndPoint: VectorLike, angleDegrees: float
@@ -3077,10 +3045,10 @@ def cutThruAll(self, clean: bool = True, taper: float = 0) -> "Workplane":
30773045
# if no faces on the stack take the nearest face parallel to the plane zDir
30783046
if not faceRef:
30793047
# first select all with faces with good orietation
3080-
sel1 = selectors.PerpendicularDirSelector(self.plane.zDir)
3048+
sel1 = PerpendicularDirSelector(self.plane.zDir)
30813049
faces = sel1.filter(solidRef.Faces())
30823050
# then select the closest
3083-
sel2 = selectors.NearestToPointSelector(self.plane.origin.toTuple())
3051+
sel2 = NearestToPointSelector(self.plane.origin.toTuple())
30843052
faceRef = sel2.filter(faces)[0]
30853053

30863054
rv = []
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import tempfile
2+
import os
3+
import io as StringIO
4+
5+
from typing import IO, Optional, Union, cast
6+
from typing_extensions import Literal
7+
8+
from ...cq import Workplane
9+
from ...utils import deprecate
10+
from ..shapes import Shape
11+
12+
from .svg import getSVG
13+
from .json import JsonMesh
14+
from .amf import AmfWriter
15+
from .dxf import exportDXF
16+
from .utils import toCompound
17+
18+
19+
class ExportTypes:
20+
STL = "STL"
21+
STEP = "STEP"
22+
AMF = "AMF"
23+
SVG = "SVG"
24+
TJS = "TJS"
25+
DXF = "DXF"
26+
27+
28+
ExportLiterals = Literal["STL", "STEP", "AMF", "SVG", "TJS", "DXF"]
29+
30+
31+
def export(
32+
w: Union[Shape, Workplane],
33+
fname: str,
34+
exportType: Optional[ExportLiterals] = None,
35+
tolerance: float = 0.1,
36+
):
37+
38+
"""
39+
Export Wokrplane or Shape to file. Multiple entities are converted to compound.
40+
41+
:param w: Shape or Wokrplane to be exported.
42+
:param fname: output filename.
43+
:param exportType: the exportFormat to use. If None will be inferred from the extension. Default: None.
44+
:param tolerance: the tolerance, in model units. Default 0.1.
45+
"""
46+
47+
shape: Shape
48+
f: IO
49+
50+
if isinstance(w, Workplane):
51+
shape = toCompound(w)
52+
else:
53+
shape = w
54+
55+
if exportType is None:
56+
t = fname.split(".")[-1].upper()
57+
if t in ExportTypes.__dict__.values():
58+
exportType = cast(ExportLiterals, t)
59+
else:
60+
raise ValueError("Unknown extensions, specify export type explicitly")
61+
62+
if exportType == ExportTypes.TJS:
63+
tess = shape.tessellate(tolerance)
64+
mesher = JsonMesh()
65+
66+
# add vertices
67+
for v in tess[0]:
68+
mesher.addVertex(v.x, v.y, v.z)
69+
70+
# add triangles
71+
for ixs in tess[1]:
72+
mesher.addTriangleFace(*ixs)
73+
74+
with open(fname, "w") as f:
75+
f.write(mesher.toJson())
76+
77+
elif exportType == ExportTypes.SVG:
78+
with open(fname, "w") as f:
79+
f.write(getSVG(shape))
80+
81+
elif exportType == ExportTypes.AMF:
82+
tess = shape.tessellate(tolerance)
83+
aw = AmfWriter(tess)
84+
with open(fname, "wb") as f:
85+
aw.writeAmf(f)
86+
87+
elif exportType == ExportTypes.DXF:
88+
if isinstance(w, Workplane):
89+
exportDXF(w, fname)
90+
else:
91+
raise ValueError("Only Workplanes can be exported as DXF")
92+
93+
elif exportType == ExportTypes.STEP:
94+
shape.exportStep(fname)
95+
96+
elif exportType == ExportTypes.STL:
97+
shape.exportStl(fname, tolerance)
98+
99+
else:
100+
raise ValueError("Unknown export type")
101+
102+
103+
@deprecate()
104+
def toString(shape, exportType, tolerance=0.1):
105+
s = StringIO.StringIO()
106+
exportShape(shape, exportType, s, tolerance)
107+
return s.getvalue()
108+
109+
110+
@deprecate()
111+
def exportShape(
112+
w: Union[Shape, Workplane],
113+
exportType: ExportLiterals,
114+
fileLike: IO,
115+
tolerance: float = 0.1,
116+
):
117+
"""
118+
:param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery
119+
object, the first value is exported
120+
:param exportType: the exportFormat to use
121+
:param tolerance: the tolerance, in model units
122+
:param fileLike: a file like object to which the content will be written.
123+
The object should be already open and ready to write. The caller is responsible
124+
for closing the object
125+
"""
126+
127+
def tessellate(shape):
128+
129+
return shape.tessellate(tolerance)
130+
131+
shape: Shape
132+
if isinstance(w, Workplane):
133+
shape = toCompound(w)
134+
else:
135+
shape = w
136+
137+
if exportType == ExportTypes.TJS:
138+
tess = tessellate(shape)
139+
mesher = JsonMesh()
140+
141+
# add vertices
142+
for v in tess[0]:
143+
mesher.addVertex(v.x, v.y, v.z)
144+
145+
# add triangles
146+
for t in tess[1]:
147+
mesher.addTriangleFace(*t)
148+
149+
fileLike.write(mesher.toJson())
150+
151+
elif exportType == ExportTypes.SVG:
152+
fileLike.write(getSVG(shape))
153+
elif exportType == ExportTypes.AMF:
154+
tess = tessellate(shape)
155+
aw = AmfWriter(tess)
156+
aw.writeAmf(fileLike)
157+
else:
158+
159+
# all these types required writing to a file and then
160+
# re-reading. this is due to the fact that FreeCAD writes these
161+
(h, outFileName) = tempfile.mkstemp()
162+
# weird, but we need to close this file. the next step is going to write to
163+
# it from c code, so it needs to be closed.
164+
os.close(h)
165+
166+
if exportType == ExportTypes.STEP:
167+
shape.exportStep(outFileName)
168+
elif exportType == ExportTypes.STL:
169+
shape.exportStl(outFileName, tolerance)
170+
else:
171+
raise ValueError("No idea how i got here")
172+
173+
res = readAndDeleteFile(outFileName)
174+
fileLike.write(res)
175+
176+
177+
@deprecate()
178+
def readAndDeleteFile(fileName):
179+
"""
180+
read data from file provided, and delete it when done
181+
return the contents as a string
182+
"""
183+
res = ""
184+
with open(fileName, "r") as f:
185+
res = "{}".format(f.read())
186+
187+
os.remove(fileName)
188+
return res

cadquery/occ_impl/exporters/amf.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import xml.etree.cElementTree as ET
2+
3+
4+
class AmfWriter(object):
5+
def __init__(self, tessellation):
6+
7+
self.units = "mm"
8+
self.tessellation = tessellation
9+
10+
def writeAmf(self, outFile):
11+
amf = ET.Element("amf", units=self.units)
12+
# TODO: if result is a compound, we need to loop through them
13+
object = ET.SubElement(amf, "object", id="0")
14+
mesh = ET.SubElement(object, "mesh")
15+
vertices = ET.SubElement(mesh, "vertices")
16+
volume = ET.SubElement(mesh, "volume")
17+
18+
# add vertices
19+
for v in self.tessellation[0]:
20+
vtx = ET.SubElement(vertices, "vertex")
21+
coord = ET.SubElement(vtx, "coordinates")
22+
x = ET.SubElement(coord, "x")
23+
x.text = str(v.x)
24+
y = ET.SubElement(coord, "y")
25+
y.text = str(v.y)
26+
z = ET.SubElement(coord, "z")
27+
z.text = str(v.z)
28+
29+
# add triangles
30+
for t in self.tessellation[1]:
31+
triangle = ET.SubElement(volume, "triangle")
32+
v1 = ET.SubElement(triangle, "v1")
33+
v1.text = str(t[0])
34+
v2 = ET.SubElement(triangle, "v2")
35+
v2.text = str(t[1])
36+
v3 = ET.SubElement(triangle, "v3")
37+
v3.text = str(t[2])
38+
39+
amf = ET.ElementTree(amf).write(outFile, xml_declaration=True)

0 commit comments

Comments
 (0)