Skip to content

Commit

Permalink
Merge pull request #415 from CadQuery/dxf-export
Browse files Browse the repository at this point in the history
DXF export (2D) and exporters cleanup
  • Loading branch information
jmwright authored Jul 30, 2020
2 parents 446cff7 + 62746e3 commit 03fad35
Show file tree
Hide file tree
Showing 14 changed files with 612 additions and 347 deletions.
3 changes: 2 additions & 1 deletion cadquery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
StringSyntaxSelector,
Selector,
)
from .cq import CQ, Workplane, selectors
from .cq import CQ, Workplane
from . import selectors
from . import plugins


Expand Down
66 changes: 17 additions & 49 deletions cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,30 @@
)
from typing_extensions import Literal

from . import (
Vector,
Plane,
Matrix,
Location,

from .occ_impl.geom import Vector, Plane, Location
from .occ_impl.shapes import (
Shape,
Edge,
Wire,
Face,
Solid,
Compound,
sortWiresByBuildOrder,
selectors,
exporters,
)

from .occ_impl.exporters.svg import getSVG, exportSVG

from .utils import deprecate_kwarg, deprecate

from .selectors import (
Selector,
PerpendicularDirSelector,
NearestToPointSelector,
StringSyntaxSelector,
)

CQObject = Union[Vector, Location, Shape]
Selector = selectors.Selector
VectorLike = Union[Tuple[float, float], Tuple[float, float, float], Vector]


Expand Down Expand Up @@ -180,42 +184,6 @@ def __init__(
self.ctx = CQContext()
self._tag = None

'''
def __init__(self, obj: Optional[CQObject]) -> None:
"""
Construct a new CadQuery (CQ) object that wraps a CAD primitive.
:param obj: Object to Wrap.
:type obj: A CAD Primitive ( wire,vertex,face,solid,edge )
"""
self.objects = []
self.ctx = CQContext()
self.parent = None
self._tag = None
if obj: # guarded because sometimes None for internal use
self.objects.append(obj)
def newObject(self, objlist: Sequence[CQObject]) -> "Workplane":
"""
Make a new CQ object.
:param objlist: The stack of objects to use
:type objlist: a list of CAD primitives ( wire,face,edge,solid,vertex,etc )
The parent of the new object will be set to the current object,
to preserve the chain correctly.
Custom plugins and subclasses should use this method to create new CQ objects
correctly.
"""
r = Workplane() # create a completely blank one
r.parent = self
r.ctx = self.ctx # context solid remains the same
r.objects = list(objlist)
return r
'''

def tag(self, name: str) -> "Workplane":
"""
Tags the current CQ object for later reference.
Expand Down Expand Up @@ -753,7 +721,7 @@ def _selectObjects(
selectorObj: Selector
if selector:
if isinstance(selector, str):
selectorObj = selectors.StringSyntaxSelector(selector)
selectorObj = StringSyntaxSelector(selector)
else:
selectorObj = selector
toReturn = selectorObj.filter(toReturn)
Expand Down Expand Up @@ -979,7 +947,7 @@ def toSvg(self, opts: Any = None) -> str:
:type opts: dictionary, width and height
:return: a string that contains SVG that represents this item.
"""
return exporters.getSVG(self.val(), opts)
return getSVG(self.val(), opts)

def exportSvg(self, fileName: str) -> None:
"""
Expand All @@ -990,7 +958,7 @@ def exportSvg(self, fileName: str) -> None:
:param fileName: the filename to export
:type fileName: String, absolute path to the file
"""
exporters.exportSVG(self, fileName)
exportSVG(self, fileName)

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

rv = []
Expand Down
188 changes: 188 additions & 0 deletions cadquery/occ_impl/exporters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import tempfile
import os
import io as StringIO

from typing import IO, Optional, Union, cast
from typing_extensions import Literal

from ...cq import Workplane
from ...utils import deprecate
from ..shapes import Shape

from .svg import getSVG
from .json import JsonMesh
from .amf import AmfWriter
from .dxf import exportDXF
from .utils import toCompound


class ExportTypes:
STL = "STL"
STEP = "STEP"
AMF = "AMF"
SVG = "SVG"
TJS = "TJS"
DXF = "DXF"


ExportLiterals = Literal["STL", "STEP", "AMF", "SVG", "TJS", "DXF"]


def export(
w: Union[Shape, Workplane],
fname: str,
exportType: Optional[ExportLiterals] = None,
tolerance: float = 0.1,
):

"""
Export Wokrplane or Shape to file. Multiple entities are converted to compound.
:param w: Shape or Wokrplane to be exported.
:param fname: output filename.
:param exportType: the exportFormat to use. If None will be inferred from the extension. Default: None.
:param tolerance: the tolerance, in model units. Default 0.1.
"""

shape: Shape
f: IO

if isinstance(w, Workplane):
shape = toCompound(w)
else:
shape = w

if exportType is None:
t = fname.split(".")[-1].upper()
if t in ExportTypes.__dict__.values():
exportType = cast(ExportLiterals, t)
else:
raise ValueError("Unknown extensions, specify export type explicitly")

if exportType == ExportTypes.TJS:
tess = shape.tessellate(tolerance)
mesher = JsonMesh()

# add vertices
for v in tess[0]:
mesher.addVertex(v.x, v.y, v.z)

# add triangles
for ixs in tess[1]:
mesher.addTriangleFace(*ixs)

with open(fname, "w") as f:
f.write(mesher.toJson())

elif exportType == ExportTypes.SVG:
with open(fname, "w") as f:
f.write(getSVG(shape))

elif exportType == ExportTypes.AMF:
tess = shape.tessellate(tolerance)
aw = AmfWriter(tess)
with open(fname, "wb") as f:
aw.writeAmf(f)

elif exportType == ExportTypes.DXF:
if isinstance(w, Workplane):
exportDXF(w, fname)
else:
raise ValueError("Only Workplanes can be exported as DXF")

elif exportType == ExportTypes.STEP:
shape.exportStep(fname)

elif exportType == ExportTypes.STL:
shape.exportStl(fname, tolerance)

else:
raise ValueError("Unknown export type")


@deprecate()
def toString(shape, exportType, tolerance=0.1):
s = StringIO.StringIO()
exportShape(shape, exportType, s, tolerance)
return s.getvalue()


@deprecate()
def exportShape(
w: Union[Shape, Workplane],
exportType: ExportLiterals,
fileLike: IO,
tolerance: float = 0.1,
):
"""
:param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery
object, the first value is exported
:param exportType: the exportFormat to use
:param tolerance: the tolerance, in model units
:param fileLike: a file like object to which the content will be written.
The object should be already open and ready to write. The caller is responsible
for closing the object
"""

def tessellate(shape):

return shape.tessellate(tolerance)

shape: Shape
if isinstance(w, Workplane):
shape = toCompound(w)
else:
shape = w

if exportType == ExportTypes.TJS:
tess = tessellate(shape)
mesher = JsonMesh()

# add vertices
for v in tess[0]:
mesher.addVertex(v.x, v.y, v.z)

# add triangles
for t in tess[1]:
mesher.addTriangleFace(*t)

fileLike.write(mesher.toJson())

elif exportType == ExportTypes.SVG:
fileLike.write(getSVG(shape))
elif exportType == ExportTypes.AMF:
tess = tessellate(shape)
aw = AmfWriter(tess)
aw.writeAmf(fileLike)
else:

# all these types required writing to a file and then
# re-reading. this is due to the fact that FreeCAD writes these
(h, outFileName) = tempfile.mkstemp()
# weird, but we need to close this file. the next step is going to write to
# it from c code, so it needs to be closed.
os.close(h)

if exportType == ExportTypes.STEP:
shape.exportStep(outFileName)
elif exportType == ExportTypes.STL:
shape.exportStl(outFileName, tolerance)
else:
raise ValueError("No idea how i got here")

res = readAndDeleteFile(outFileName)
fileLike.write(res)


@deprecate()
def readAndDeleteFile(fileName):
"""
read data from file provided, and delete it when done
return the contents as a string
"""
res = ""
with open(fileName, "r") as f:
res = "{}".format(f.read())

os.remove(fileName)
return res
39 changes: 39 additions & 0 deletions cadquery/occ_impl/exporters/amf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import xml.etree.cElementTree as ET


class AmfWriter(object):
def __init__(self, tessellation):

self.units = "mm"
self.tessellation = tessellation

def writeAmf(self, outFile):
amf = ET.Element("amf", units=self.units)
# TODO: if result is a compound, we need to loop through them
object = ET.SubElement(amf, "object", id="0")
mesh = ET.SubElement(object, "mesh")
vertices = ET.SubElement(mesh, "vertices")
volume = ET.SubElement(mesh, "volume")

# add vertices
for v in self.tessellation[0]:
vtx = ET.SubElement(vertices, "vertex")
coord = ET.SubElement(vtx, "coordinates")
x = ET.SubElement(coord, "x")
x.text = str(v.x)
y = ET.SubElement(coord, "y")
y.text = str(v.y)
z = ET.SubElement(coord, "z")
z.text = str(v.z)

# add triangles
for t in self.tessellation[1]:
triangle = ET.SubElement(volume, "triangle")
v1 = ET.SubElement(triangle, "v1")
v1.text = str(t[0])
v2 = ET.SubElement(triangle, "v2")
v2.text = str(t[1])
v3 = ET.SubElement(triangle, "v3")
v3.text = str(t[2])

amf = ET.ElementTree(amf).write(outFile, xml_declaration=True)
Loading

0 comments on commit 03fad35

Please sign in to comment.