Skip to content

Commit 36d4178

Browse files
jmwrightmarcus7070adam-urbanczyk
authored
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 <[email protected]> * 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 <[email protected]> Co-authored-by: Adam Urbańczyk <[email protected]>
1 parent 3348c18 commit 36d4178

File tree

3 files changed

+112
-38
lines changed

3 files changed

+112
-38
lines changed

cadquery/occ_impl/exporters/__init__.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,18 @@ def export(
3737
exportType: Optional[ExportLiterals] = None,
3838
tolerance: float = 0.1,
3939
angularTolerance: float = 0.1,
40+
opt=None,
4041
):
4142

4243
"""
4344
Export Wokrplane or Shape to file. Multiple entities are converted to compound.
44-
45+
4546
:param w: Shape or Wokrplane to be exported.
4647
:param fname: output filename.
4748
:param exportType: the exportFormat to use. If None will be inferred from the extension. Default: None.
4849
:param tolerance: the deflection tolerance, in model units. Default 0.1.
4950
:param angularTolerance: the angular tolerance, in radians. Default 0.1.
51+
:param opt: additional options passed to the specific exporter. Default None.
5052
"""
5153

5254
shape: Shape
@@ -81,7 +83,7 @@ def export(
8183

8284
elif exportType == ExportTypes.SVG:
8385
with open(fname, "w") as f:
84-
f.write(getSVG(shape))
86+
f.write(getSVG(shape, opt))
8587

8688
elif exportType == ExportTypes.AMF:
8789
tess = shape.tessellate(tolerance, angularTolerance)
@@ -125,14 +127,14 @@ def exportShape(
125127
angularTolerance: float = 0.1,
126128
):
127129
"""
128-
:param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery
129-
object, the first value is exported
130-
:param exportType: the exportFormat to use
131-
:param fileLike: a file like object to which the content will be written.
132-
The object should be already open and ready to write. The caller is responsible
133-
for closing the object
134-
:param tolerance: the linear tolerance, in model units. Default 0.1.
135-
:param angularTolerance: the angular tolerance, in radians. Default 0.1.
130+
:param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery
131+
object, the first value is exported
132+
:param exportType: the exportFormat to use
133+
:param fileLike: a file like object to which the content will be written.
134+
The object should be already open and ready to write. The caller is responsible
135+
for closing the object
136+
:param tolerance: the linear tolerance, in model units. Default 0.1.
137+
:param angularTolerance: the angular tolerance, in radians. Default 0.1.
136138
"""
137139

138140
def tessellate(shape, angularTolerance):
@@ -188,8 +190,8 @@ def tessellate(shape, angularTolerance):
188190
@deprecate()
189191
def readAndDeleteFile(fileName):
190192
"""
191-
read data from file provided, and delete it when done
192-
return the contents as a string
193+
Read data from file provided, and delete it when done
194+
return the contents as a string
193195
"""
194196
res = ""
195197
with open(fileName, "r") as f:

cadquery/occ_impl/exporters/svg.py

Lines changed: 78 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from OCP.GCPnts import GCPnts_QuasiUniformDeflection
1212

1313
DISCRETIZATION_TOLERANCE = 1e-3
14-
DEFAULT_DIR = gp_Dir(-1.75, 1.1, 5)
1514

1615
SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1716
<svg
@@ -23,16 +22,21 @@
2322
>
2423
<g transform="scale(%(unitScale)s, -%(unitScale)s) translate(%(xTranslate)s,%(yTranslate)s)" stroke-width="%(strokeWidth)s" fill="none">
2524
<!-- hidden lines -->
26-
<g stroke="rgb(160, 160, 160)" fill="none" stroke-dasharray="%(strokeWidth)s,%(strokeWidth)s" >
25+
<g stroke="rgb(%(hiddenColor)s)" fill="none" stroke-dasharray="%(strokeWidth)s,%(strokeWidth)s" >
2726
%(hiddenContent)s
2827
</g>
2928
3029
<!-- solid lines -->
31-
<g stroke="rgb(0, 0, 0)" fill="none">
30+
<g stroke="rgb(%(strokeColor)s)" fill="none">
3231
%(visibleContent)s
3332
</g>
3433
</g>
35-
<g transform="translate(20,%(textboxY)s)" stroke="rgb(0,0,255)">
34+
%(axesIndicator)s
35+
</svg>
36+
"""
37+
38+
# The axes indicator - needs to be replaced with something dynamic eventually
39+
AXES_TEMPLATE = """<g transform="translate(20,%(textboxY)s)" stroke="rgb(0,0,255)">
3640
<line x1="30" y1="-30" x2="75" y2="-33" stroke-width="3" stroke="#000000" />
3741
<text x="80" y="-30" style="stroke:#000000">X </text>
3842
@@ -45,9 +49,7 @@
4549
<line x1="0" y1="0" x2="%(unitScale)s" y2="0" stroke-width="3" />
4650
<text x="0" y="20" style="stroke:#000000">1 %(uom)s </text>
4751
-->
48-
</g>
49-
</svg>
50-
"""
52+
</g>"""
5153

5254
PATHTEMPLATE = '\t\t\t<path d="%s" />\n'
5355

@@ -59,7 +61,7 @@ class UNITS:
5961

6062
def guessUnitOfMeasure(shape):
6163
"""
62-
Guess the unit of measure of a shape.
64+
Guess the unit of measure of a shape.
6365
"""
6466
bb = BoundBox._fromTopoDS(shape.wrapped)
6567

@@ -81,7 +83,7 @@ def guessUnitOfMeasure(shape):
8183

8284
def makeSVGedge(e):
8385
"""
84-
86+
Creates an SVG edge from a OCCT edge.
8587
"""
8688

8789
cs = StringIO.StringIO()
@@ -106,7 +108,7 @@ def makeSVGedge(e):
106108

107109
def getPaths(visibleShapes, hiddenShapes):
108110
"""
109-
111+
Collects the visible and hidden edges from the CadQuery object.
110112
"""
111113

112114
hiddenPaths = []
@@ -125,10 +127,38 @@ def getPaths(visibleShapes, hiddenShapes):
125127

126128
def getSVG(shape, opts=None):
127129
"""
128-
Export a shape to SVG
130+
Export a shape to SVG text.
131+
132+
:param shape: A CadQuery shape object to convert to an SVG string.
133+
:type Shape: Vertex, Edge, Wire, Face, Shell, Solid, or Compound.
134+
:param opts: An options dictionary that influences the SVG that is output.
135+
:type opts: Dictionary, keys are as follows:
136+
width: Document width of the resulting image.
137+
height: Document height of the resulting image.
138+
marginLeft: Inset margin from the left side of the document.
139+
marginTop: Inset margin from the top side of the document.
140+
projectionDir: Direction the camera will view the shape from.
141+
showAxes: Whether or not to show the axes indicator, which will only be
142+
visible when the projectionDir is also at the default.
143+
strokeWidth: Width of the line that visible edges are drawn with.
144+
strokeColor: Color of the line that visible edges are drawn with.
145+
hiddenColor: Color of the line that hidden edges are drawn with.
146+
showHidden: Whether or not to show hidden lines.
129147
"""
130148

131-
d = {"width": 800, "height": 240, "marginLeft": 200, "marginTop": 20}
149+
# Available options and their defaults
150+
d = {
151+
"width": 800,
152+
"height": 240,
153+
"marginLeft": 200,
154+
"marginTop": 20,
155+
"projectionDir": (-1.75, 1.1, 5),
156+
"showAxes": True,
157+
"strokeWidth": -1.0, # -1 = calculated based on unitScale
158+
"strokeColor": (0, 0, 0), # RGB 0-255
159+
"hiddenColor": (160, 160, 160), # RGB 0-255
160+
"showHidden": True,
161+
}
132162

133163
if opts:
134164
d.update(opts)
@@ -140,11 +170,17 @@ def getSVG(shape, opts=None):
140170
height = float(d["height"])
141171
marginLeft = float(d["marginLeft"])
142172
marginTop = float(d["marginTop"])
173+
projectionDir = tuple(d["projectionDir"])
174+
showAxes = bool(d["showAxes"])
175+
strokeWidth = float(d["strokeWidth"])
176+
strokeColor = tuple(d["strokeColor"])
177+
hiddenColor = tuple(d["hiddenColor"])
178+
showHidden = bool(d["showHidden"])
143179

144180
hlr = HLRBRep_Algo()
145181
hlr.Add(shape.wrapped)
146182

147-
projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), DEFAULT_DIR))
183+
projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), gp_Dir(*projectionDir)))
148184

149185
hlr.Projector(projector)
150186
hlr.Update()
@@ -190,7 +226,7 @@ def getSVG(shape, opts=None):
190226
# get bounding box -- these are all in 2-d space
191227
bb = Compound.makeCompound(hidden + visible).BoundingBox()
192228

193-
# width pixels for x, height pixesl for y
229+
# width pixels for x, height pixels for y
194230
unitScale = min(width / bb.xlen * 0.75, height / bb.ylen * 0.75)
195231

196232
# compute amount to translate-- move the top left into view
@@ -199,19 +235,36 @@ def getSVG(shape, opts=None):
199235
(0 - bb.ymax) - marginTop / unitScale,
200236
)
201237

202-
# compute paths ( again -- had to strip out freecad crap )
238+
# If the user did not specify a stroke width, calculate it based on the unit scale
239+
if strokeWidth == -1.0:
240+
strokeWidth = 1.0 / unitScale
241+
242+
# compute paths
203243
hiddenContent = ""
204-
for p in hiddenPaths:
205-
hiddenContent += PATHTEMPLATE % p
244+
245+
# Prevent hidden paths from being added if the user disabled them
246+
if showHidden:
247+
for p in hiddenPaths:
248+
hiddenContent += PATHTEMPLATE % p
206249

207250
visibleContent = ""
208251
for p in visiblePaths:
209252
visibleContent += PATHTEMPLATE % p
210253

254+
# If the caller wants the axes indicator and is using the default direction, add in the indicator
255+
if showAxes and projectionDir == (-1.75, 1.1, 5):
256+
axesIndicator = AXES_TEMPLATE % (
257+
{"unitScale": str(unitScale), "textboxY": str(height - 30), "uom": str(uom)}
258+
)
259+
else:
260+
axesIndicator = ""
261+
211262
svg = SVG_TEMPLATE % (
212263
{
213264
"unitScale": str(unitScale),
214-
"strokeWidth": str(1.0 / unitScale),
265+
"strokeWidth": str(strokeWidth),
266+
"strokeColor": ",".join([str(x) for x in strokeColor]),
267+
"hiddenColor": ",".join([str(x) for x in hiddenColor]),
215268
"hiddenContent": hiddenContent,
216269
"visibleContent": visibleContent,
217270
"xTranslate": str(xTranslate),
@@ -220,22 +273,21 @@ def getSVG(shape, opts=None):
220273
"height": str(height),
221274
"textboxY": str(height - 30),
222275
"uom": str(uom),
276+
"axesIndicator": axesIndicator,
223277
}
224278
)
225-
# svg = SVG_TEMPLATE % (
226-
# {"content": projectedContent}
227-
# )
279+
228280
return svg
229281

230282

231-
def exportSVG(shape, fileName: str):
283+
def exportSVG(shape, fileName: str, opts=None):
232284
"""
233-
accept a cadquery shape, and export it to the provided file
234-
TODO: should use file-like objects, not a fileName, and/or be able to return a string instead
235-
export a view of a part to svg
285+
Accept a cadquery shape, and export it to the provided file
286+
TODO: should use file-like objects, not a fileName, and/or be able to return a string instead
287+
export a view of a part to svg
236288
"""
237289

238-
svg = getSVG(shape.val())
290+
svg = getSVG(shape.val(), opts)
239291
f = open(fileName, "w")
240292
f.write(svg)
241293
f.close()

tests/test_exporters.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,26 @@ def testSVG(self):
4949

5050
exporters.export(self._box(), "out.svg")
5151

52+
def testSVGOptions(self):
53+
self._exportBox(exporters.ExportTypes.SVG, ["<svg", "<g transform"])
54+
55+
exporters.export(
56+
self._box(),
57+
"out.svg",
58+
opt={
59+
"width": 100,
60+
"height": 100,
61+
"marginLeft": 10,
62+
"marginTop": 10,
63+
"showAxes": False,
64+
"projectionDir": (0, 0, 1),
65+
"strokeWidth": 0.25,
66+
"strokeColor": (255, 0, 0),
67+
"hiddenColor": (0, 0, 255),
68+
"showHidden": True,
69+
},
70+
)
71+
5272
def testAMF(self):
5373
self._exportBox(exporters.ExportTypes.AMF, ["<amf units", "</object>"])
5474

0 commit comments

Comments
 (0)