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:
@ -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:
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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>"])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user