Skip to content

exportSVG produces svgs with incorrect size. #1317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 48 additions & 53 deletions cadquery/occ_impl/exporters/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from ..shapes import Shape, Compound, TOLERANCE
from ..geom import BoundBox


from OCP.gp import gp_Ax2, gp_Pnt, gp_Dir
from OCP.BRepLib import BRepLib
from OCP.HLRBRep import HLRBRep_Algo, HLRBRep_HLRToShape
Expand All @@ -12,26 +11,22 @@

DISCRETIZATION_TOLERANCE = 1e-3

SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="%(width)s"
height="%(height)s"

>
<g transform="scale(%(unitScale)s, -%(unitScale)s) translate(%(xTranslate)s,%(yTranslate)s)" stroke-width="%(strokeWidth)s" fill="none">
<!-- hidden lines -->
<g stroke="rgb(%(hiddenColor)s)" fill="none" stroke-dasharray="%(strokeWidth)s,%(strokeWidth)s" >
SVG_TEMPLATE = """<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<svg baseProfile="tiny" version="1.2" width="%(width)s" height="%(height)s" xmlns:cc="http://creativecommons.org/ns#" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:svg="http://www.w3.org/2000/svg">
<title>CadQuery SVG Export</title>
<desc>Page exported from CadQuery document</desc>
<defs/>
<g id="DrawingContent" stroke-linejoin="bevel" stroke-linecap="square" stroke="black" fill="none" fill-rule="evenodd" stroke-width="%(strokeWidth)s">
<!-- hidden lines -->
<g stroke="rgb(%(hiddenColor)s)" fill="none" stroke-dasharray="%(strokeWidth)s,%(strokeWidth)s" >
%(hiddenContent)s
</g>

<!-- solid lines -->
<g stroke="rgb(%(strokeColor)s)" fill="none">
</g>
<!-- solid lines -->
<g stroke="rgb(%(strokeColor)s)" fill="none">
%(visibleContent)s
</g>
</g>
%(axesIndicator)s
</g>
</g>
%(axesIndicator)s
</svg>
"""

Expand All @@ -51,36 +46,14 @@
-->
</g>"""

PATHTEMPLATE = '\t\t\t<path d="%s" />\n'
PATHTEMPLATE = ' <g stroke-opacity="1" stroke-linejoin="bevel" font-style="normal" stroke-linecap="square" font-family="MS Shell Dlg 2" stroke="rgb(%(strokeColor)s)" fill="none" font-weight="400" transform="scale(%(unitScale)s, %(unitScale)s) translate(%(xTranslate)s,%(yTranslate)s)" stroke-width="%(strokeWidth)s" font-size="55.5625">\n <path vector-effect="none" d="%(path)s" fill-rule="evenodd"/>\n </g>\n'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transform="scale(%(unitScale)s, %(unitScale)s)

Y axis should (must?) be flipped here (like in the old implementation). CQ/OpenCascade has Y axis moving upwards whereas SVG has Y axis moving downwards. Without flipping it the resulting SVG will be a horizontal mirror of the actual projection.



class UNITS:
MM = "mm"
IN = "in"


def guessUnitOfMeasure(shape):
"""
Guess the unit of measure of a shape.
"""
bb = BoundBox._fromTopoDS(shape.wrapped)

dimList = [bb.xlen, bb.ylen, bb.zlen]
# no real part would likely be bigger than 10 inches on any side
if max(dimList) > 10:
return UNITS.MM

# no real part would likely be smaller than 0.1 mm on all dimensions
if min(dimList) < 0.1:
return UNITS.IN

# no real part would have the sum of its dimensions less than about 5mm
if sum(dimList) < 10:
return UNITS.IN

return UNITS.MM


def makeSVGedge(e):
"""
Creates an SVG edge from a OCCT edge.
Expand Down Expand Up @@ -125,7 +98,7 @@ def getPaths(visibleShapes, hiddenShapes):
return (hiddenPaths, visiblePaths)


def getSVG(shape, opts=None):
def getSVG(shapes, opts=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since accidentally having the wrong unit could lead to unnecessary end user headache, I would consider moving unitOfMeasure all the way as a separate kwarg to make it more obvious to the user.

Suggested change
def getSVG(shapes, opts=None):
def getSVG(shapes, unitOfMeasure=UNITS.MM, opts=None):

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shape -> shapes is a breaking change (and in fact breaks 18 tests). Backwards compatibility could be maintained by allowing both single and iterable inputs

"""
Export a shape to SVG text.

Expand All @@ -150,9 +123,10 @@ def getSVG(shape, opts=None):

# Available options and their defaults
d = {
"unitOfMeasure": UNITS.MM,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"unitOfMeasure": UNITS.MM,
"unitOfMeasure": unitOfMeasure,

Also, document in the docstring :param unitOfMeasure: above :param opts: (line 107)

If the suggestion is not implemented, unitOfMeasure needs to be still added to the opts documentation.

"width": 800,
"height": 240,
"marginLeft": 200,
"height": 500,
"marginLeft": 20,
"marginTop": 20,
"projectionDir": (-1.75, 1.1, 5),
"showAxes": True,
Expand All @@ -166,9 +140,7 @@ def getSVG(shape, opts=None):
if opts:
d.update(opts)

# need to guess the scale and the coordinate center
uom = guessUnitOfMeasure(shape)

uom = d["unitOfMeasure"]
width = float(d["width"])
height = float(d["height"])
marginLeft = float(d["marginLeft"])
Expand All @@ -182,7 +154,9 @@ def getSVG(shape, opts=None):
focus = float(d["focus"]) if d.get("focus") else None

hlr = HLRBRep_Algo()
hlr.Add(shape.wrapped)

for shape in shapes:
hlr.Add(shape.wrapped)

coordinate_system = gp_Ax2(gp_Pnt(), gp_Dir(*projectionDir))

Expand Down Expand Up @@ -236,12 +210,15 @@ def getSVG(shape, opts=None):
bb = Compound.makeCompound(hidden + visible).BoundingBox()

# width pixels for x, height pixels for y
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment not relevant anymore

unitScale = min(width / bb.xlen * 0.75, height / bb.ylen * 0.75)
if uom == UNITS.MM:
unitScale = 3.7795
else:
unitScale = 91

# compute amount to translate-- move the top left into view
(xTranslate, yTranslate) = (
(0 - bb.xmin) + marginLeft / unitScale,
(0 - bb.ymax) - marginTop / unitScale,
(0 + bb.ymax) + marginTop / unitScale,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be necessary if the Y axis is flipped. (ln 49)

)

# If the user did not specify a stroke width, calculate it based on the unit scale
Expand All @@ -254,11 +231,29 @@ def getSVG(shape, opts=None):
# Prevent hidden paths from being added if the user disabled them
if showHidden:
for p in hiddenPaths:
hiddenContent += PATHTEMPLATE % p
hiddenContent += PATHTEMPLATE % (
{
"unitScale": str(unitScale),
"xTranslate": str(xTranslate),
"yTranslate": str(yTranslate),
"strokeWidth": str(strokeWidth),
"strokeColor": ",".join([str(x) for x in strokeColor]),
"path": str(p),
}
)

visibleContent = ""
for p in visiblePaths:
visibleContent += PATHTEMPLATE % p
visibleContent += PATHTEMPLATE % (
{
"unitScale": str(unitScale),
"xTranslate": str(xTranslate),
"yTranslate": str(yTranslate),
"strokeWidth": str(strokeWidth),
"strokeColor": ",".join([str(x) for x in strokeColor]),
"path": str(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):
Expand Down