Expose SVG Export Options and Add Some Useful Extras (#596)

* Exposed getSVG opt parameter and added project dir

* Added additional options based on feedback

* Lint updates

* Added docstring for getSVG

* Fix strange black formatting suggestion.

Co-authored-by: Marcus Boyd <50230945+marcus7070@users.noreply.github.com>

* Fixed another Lint error that I did not see locally

* Added test for STL options

* Trying to get codecov to see that line 260 is covered

* Fixed copy-paste error of the wrong test

* Mention opt in the docstring

Co-authored-by: Marcus Boyd <50230945+marcus7070@users.noreply.github.com>
Co-authored-by: Adam Urbańczyk <adam-urbanczyk@users.noreply.github.com>
This commit is contained in:
Jeremy Wright
2021-01-28 16:04:35 -05:00
committed by GitHub
parent 3348c18889
commit 36d4178024
3 changed files with 112 additions and 38 deletions

View File

@ -37,16 +37,18 @@ def export(
exportType: Optional[ExportLiterals] = None, exportType: Optional[ExportLiterals] = None,
tolerance: float = 0.1, tolerance: float = 0.1,
angularTolerance: float = 0.1, angularTolerance: float = 0.1,
opt=None,
): ):
""" """
Export Wokrplane or Shape to file. Multiple entities are converted to compound. Export Wokrplane or Shape to file. Multiple entities are converted to compound.
:param w: Shape or Wokrplane to be exported. :param w: Shape or Wokrplane to be exported.
:param fname: output filename. :param fname: output filename.
:param exportType: the exportFormat to use. If None will be inferred from the extension. Default: None. :param exportType: the exportFormat to use. If None will be inferred from the extension. Default: None.
:param tolerance: the deflection tolerance, in model units. Default 0.1. :param tolerance: the deflection tolerance, in model units. Default 0.1.
:param angularTolerance: the angular tolerance, in radians. Default 0.1. :param angularTolerance: the angular tolerance, in radians. Default 0.1.
:param opt: additional options passed to the specific exporter. Default None.
""" """
shape: Shape shape: Shape
@ -81,7 +83,7 @@ def export(
elif exportType == ExportTypes.SVG: elif exportType == ExportTypes.SVG:
with open(fname, "w") as f: with open(fname, "w") as f:
f.write(getSVG(shape)) f.write(getSVG(shape, opt))
elif exportType == ExportTypes.AMF: elif exportType == ExportTypes.AMF:
tess = shape.tessellate(tolerance, angularTolerance) tess = shape.tessellate(tolerance, angularTolerance)
@ -125,14 +127,14 @@ def exportShape(
angularTolerance: float = 0.1, angularTolerance: float = 0.1,
): ):
""" """
:param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery :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 object, the first value is exported
:param exportType: the exportFormat to use :param exportType: the exportFormat to use
:param fileLike: a file like object to which the content will be written. :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 The object should be already open and ready to write. The caller is responsible
for closing the object for closing the object
:param tolerance: the linear tolerance, in model units. Default 0.1. :param tolerance: the linear tolerance, in model units. Default 0.1.
:param angularTolerance: the angular tolerance, in radians. Default 0.1. :param angularTolerance: the angular tolerance, in radians. Default 0.1.
""" """
def tessellate(shape, angularTolerance): def tessellate(shape, angularTolerance):
@ -188,8 +190,8 @@ def exportShape(
@deprecate() @deprecate()
def readAndDeleteFile(fileName): def readAndDeleteFile(fileName):
""" """
read data from file provided, and delete it when done Read data from file provided, and delete it when done
return the contents as a string return the contents as a string
""" """
res = "" res = ""
with open(fileName, "r") as f: with open(fileName, "r") as f:

View File

@ -11,7 +11,6 @@ from OCP.HLRAlgo import HLRAlgo_Projector
from OCP.GCPnts import GCPnts_QuasiUniformDeflection from OCP.GCPnts import GCPnts_QuasiUniformDeflection
DISCRETIZATION_TOLERANCE = 1e-3 DISCRETIZATION_TOLERANCE = 1e-3
DEFAULT_DIR = gp_Dir(-1.75, 1.1, 5)
SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?> SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg <svg
@ -23,16 +22,21 @@ SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
> >
<g transform="scale(%(unitScale)s, -%(unitScale)s) translate(%(xTranslate)s,%(yTranslate)s)" stroke-width="%(strokeWidth)s" fill="none"> <g transform="scale(%(unitScale)s, -%(unitScale)s) translate(%(xTranslate)s,%(yTranslate)s)" stroke-width="%(strokeWidth)s" fill="none">
<!-- hidden lines --> <!-- hidden lines -->
<g stroke="rgb(160, 160, 160)" fill="none" stroke-dasharray="%(strokeWidth)s,%(strokeWidth)s" > <g stroke="rgb(%(hiddenColor)s)" fill="none" stroke-dasharray="%(strokeWidth)s,%(strokeWidth)s" >
%(hiddenContent)s %(hiddenContent)s
</g> </g>
<!-- solid lines --> <!-- solid lines -->
<g stroke="rgb(0, 0, 0)" fill="none"> <g stroke="rgb(%(strokeColor)s)" fill="none">
%(visibleContent)s %(visibleContent)s
</g> </g>
</g> </g>
<g transform="translate(20,%(textboxY)s)" stroke="rgb(0,0,255)"> %(axesIndicator)s
</svg>
"""
# The axes indicator - needs to be replaced with something dynamic eventually
AXES_TEMPLATE = """<g transform="translate(20,%(textboxY)s)" stroke="rgb(0,0,255)">
<line x1="30" y1="-30" x2="75" y2="-33" stroke-width="3" stroke="#000000" /> <line x1="30" y1="-30" x2="75" y2="-33" stroke-width="3" stroke="#000000" />
<text x="80" y="-30" style="stroke:#000000">X </text> <text x="80" y="-30" style="stroke:#000000">X </text>
@ -45,9 +49,7 @@ SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<line x1="0" y1="0" x2="%(unitScale)s" y2="0" stroke-width="3" /> <line x1="0" y1="0" x2="%(unitScale)s" y2="0" stroke-width="3" />
<text x="0" y="20" style="stroke:#000000">1 %(uom)s </text> <text x="0" y="20" style="stroke:#000000">1 %(uom)s </text>
--> -->
</g> </g>"""
</svg>
"""
PATHTEMPLATE = '\t\t\t<path d="%s" />\n' PATHTEMPLATE = '\t\t\t<path d="%s" />\n'
@ -59,7 +61,7 @@ class UNITS:
def guessUnitOfMeasure(shape): def guessUnitOfMeasure(shape):
""" """
Guess the unit of measure of a shape. Guess the unit of measure of a shape.
""" """
bb = BoundBox._fromTopoDS(shape.wrapped) bb = BoundBox._fromTopoDS(shape.wrapped)
@ -81,7 +83,7 @@ def guessUnitOfMeasure(shape):
def makeSVGedge(e): def makeSVGedge(e):
""" """
Creates an SVG edge from a OCCT edge.
""" """
cs = StringIO.StringIO() cs = StringIO.StringIO()
@ -106,7 +108,7 @@ def makeSVGedge(e):
def getPaths(visibleShapes, hiddenShapes): def getPaths(visibleShapes, hiddenShapes):
""" """
Collects the visible and hidden edges from the CadQuery object.
""" """
hiddenPaths = [] hiddenPaths = []
@ -125,10 +127,38 @@ def getPaths(visibleShapes, hiddenShapes):
def getSVG(shape, opts=None): def getSVG(shape, opts=None):
""" """
Export a shape to SVG Export a shape to SVG text.
:param shape: A CadQuery shape object to convert to an SVG string.
:type Shape: Vertex, Edge, Wire, Face, Shell, Solid, or Compound.
:param opts: An options dictionary that influences the SVG that is output.
:type opts: Dictionary, keys are as follows:
width: Document width of the resulting image.
height: Document height of the resulting image.
marginLeft: Inset margin from the left side of the document.
marginTop: Inset margin from the top side of the document.
projectionDir: Direction the camera will view the shape from.
showAxes: Whether or not to show the axes indicator, which will only be
visible when the projectionDir is also at the default.
strokeWidth: Width of the line that visible edges are drawn with.
strokeColor: Color of the line that visible edges are drawn with.
hiddenColor: Color of the line that hidden edges are drawn with.
showHidden: Whether or not to show hidden lines.
""" """
d = {"width": 800, "height": 240, "marginLeft": 200, "marginTop": 20} # Available options and their defaults
d = {
"width": 800,
"height": 240,
"marginLeft": 200,
"marginTop": 20,
"projectionDir": (-1.75, 1.1, 5),
"showAxes": True,
"strokeWidth": -1.0, # -1 = calculated based on unitScale
"strokeColor": (0, 0, 0), # RGB 0-255
"hiddenColor": (160, 160, 160), # RGB 0-255
"showHidden": True,
}
if opts: if opts:
d.update(opts) d.update(opts)
@ -140,11 +170,17 @@ def getSVG(shape, opts=None):
height = float(d["height"]) height = float(d["height"])
marginLeft = float(d["marginLeft"]) marginLeft = float(d["marginLeft"])
marginTop = float(d["marginTop"]) marginTop = float(d["marginTop"])
projectionDir = tuple(d["projectionDir"])
showAxes = bool(d["showAxes"])
strokeWidth = float(d["strokeWidth"])
strokeColor = tuple(d["strokeColor"])
hiddenColor = tuple(d["hiddenColor"])
showHidden = bool(d["showHidden"])
hlr = HLRBRep_Algo() hlr = HLRBRep_Algo()
hlr.Add(shape.wrapped) hlr.Add(shape.wrapped)
projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), DEFAULT_DIR)) projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), gp_Dir(*projectionDir)))
hlr.Projector(projector) hlr.Projector(projector)
hlr.Update() hlr.Update()
@ -190,7 +226,7 @@ def getSVG(shape, opts=None):
# get bounding box -- these are all in 2-d space # get bounding box -- these are all in 2-d space
bb = Compound.makeCompound(hidden + visible).BoundingBox() bb = Compound.makeCompound(hidden + visible).BoundingBox()
# width pixels for x, height pixesl for y # width pixels for x, height pixels for y
unitScale = min(width / bb.xlen * 0.75, height / bb.ylen * 0.75) unitScale = min(width / bb.xlen * 0.75, height / bb.ylen * 0.75)
# compute amount to translate-- move the top left into view # compute amount to translate-- move the top left into view
@ -199,19 +235,36 @@ def getSVG(shape, opts=None):
(0 - bb.ymax) - marginTop / unitScale, (0 - bb.ymax) - marginTop / unitScale,
) )
# compute paths ( again -- had to strip out freecad crap ) # If the user did not specify a stroke width, calculate it based on the unit scale
if strokeWidth == -1.0:
strokeWidth = 1.0 / unitScale
# compute paths
hiddenContent = "" hiddenContent = ""
for p in hiddenPaths:
hiddenContent += PATHTEMPLATE % p # Prevent hidden paths from being added if the user disabled them
if showHidden:
for p in hiddenPaths:
hiddenContent += PATHTEMPLATE % p
visibleContent = "" visibleContent = ""
for p in visiblePaths: for p in visiblePaths:
visibleContent += PATHTEMPLATE % p visibleContent += PATHTEMPLATE % p
# If the caller wants the axes indicator and is using the default direction, add in the indicator
if showAxes and projectionDir == (-1.75, 1.1, 5):
axesIndicator = AXES_TEMPLATE % (
{"unitScale": str(unitScale), "textboxY": str(height - 30), "uom": str(uom)}
)
else:
axesIndicator = ""
svg = SVG_TEMPLATE % ( svg = SVG_TEMPLATE % (
{ {
"unitScale": str(unitScale), "unitScale": str(unitScale),
"strokeWidth": str(1.0 / unitScale), "strokeWidth": str(strokeWidth),
"strokeColor": ",".join([str(x) for x in strokeColor]),
"hiddenColor": ",".join([str(x) for x in hiddenColor]),
"hiddenContent": hiddenContent, "hiddenContent": hiddenContent,
"visibleContent": visibleContent, "visibleContent": visibleContent,
"xTranslate": str(xTranslate), "xTranslate": str(xTranslate),
@ -220,22 +273,21 @@ def getSVG(shape, opts=None):
"height": str(height), "height": str(height),
"textboxY": str(height - 30), "textboxY": str(height - 30),
"uom": str(uom), "uom": str(uom),
"axesIndicator": axesIndicator,
} }
) )
# svg = SVG_TEMPLATE % (
# {"content": projectedContent}
# )
return svg return svg
def exportSVG(shape, fileName: str): def exportSVG(shape, fileName: str, opts=None):
""" """
accept a cadquery shape, and export it to the provided file Accept a cadquery shape, and export it to the provided file
TODO: should use file-like objects, not a fileName, and/or be able to return a string instead TODO: should use file-like objects, not a fileName, and/or be able to return a string instead
export a view of a part to svg export a view of a part to svg
""" """
svg = getSVG(shape.val()) svg = getSVG(shape.val(), opts)
f = open(fileName, "w") f = open(fileName, "w")
f.write(svg) f.write(svg)
f.close() f.close()

View File

@ -49,6 +49,26 @@ class TestExporters(BaseTest):
exporters.export(self._box(), "out.svg") exporters.export(self._box(), "out.svg")
def testSVGOptions(self):
self._exportBox(exporters.ExportTypes.SVG, ["<svg", "<g transform"])
exporters.export(
self._box(),
"out.svg",
opt={
"width": 100,
"height": 100,
"marginLeft": 10,
"marginTop": 10,
"showAxes": False,
"projectionDir": (0, 0, 1),
"strokeWidth": 0.25,
"strokeColor": (255, 0, 0),
"hiddenColor": (0, 0, 255),
"showHidden": True,
},
)
def testAMF(self): def testAMF(self):
self._exportBox(exporters.ExportTypes.AMF, ["<amf units", "</object>"]) self._exportBox(exporters.ExportTypes.AMF, ["<amf units", "</object>"])