Skip to content
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

SVG output defaults to different color for different patches #160

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
60 changes: 45 additions & 15 deletions splipy/io/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def bezier_representation(curve):

class SVG(MasterIO):

namespace = '{http://www.w3.org/2000/svg}'
namespace = '{http://www.w3.org/2000/svg}'

def __init__(self, filename, width=1000, height=1000, margin=0.05):
""" Constructor
Expand All @@ -74,8 +74,12 @@ def __init__(self, filename, width=1000, height=1000, margin=0.05):
self.width = width
self.height = height
self.margin = margin

self.default_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
self.col_i = -1

self.all_objects = []
self.all_kwargs = []

def __enter__(self):
return self
Expand All @@ -88,7 +92,7 @@ def __exit__(self, exc_type, exc_value, traceback):

# compute the bounding box for all geometries
boundingbox = [np.inf, np.inf, -np.inf, -np.inf]
for entry in self.all_objects:
for entry,args in zip(self.all_objects, self.all_kwargs):
bb = entry.bounding_box()
boundingbox[0] = min(boundingbox[0], bb[0][0])
boundingbox[1] = min(boundingbox[1], bb[1][0])
Expand Down Expand Up @@ -118,9 +122,9 @@ def __exit__(self, exc_type, exc_value, traceback):
# populate tree with all curves and surfaces in entities
for entry in self.all_objects:
if isinstance(entry, Curve):
self.write_curve(self.xmlRoot, entry)
self.write_curve(self.xmlRoot, entry, **args)
elif isinstance(entry, Surface):
self.write_surface(entry)
self.write_surface(entry, **args)

# if no objects are stored, then we've most likely only called read()
if len(self.all_objects) > 0:
Expand All @@ -129,8 +133,12 @@ def __exit__(self, exc_type, exc_value, traceback):
result = reparsed.toprettyxml(indent=" ") # adds newline and inline
f = open(self.filename, 'w')
f.write(result)

def next_color(self):
self.col_i = (self.col_i+1) % len(self.default_colors)
return self.default_colors[self.col_i]

def write_curve(self, xmlNode, curve, fill='none', stroke='#000000', width=2):
def write_curve(self, xmlNode, curve, fill='none', stroke='#000000', opacity=1.0, width=2):
""" Writes a Curve to the xml tree. This will draw a single curve

:param xmlNode: Node in xml tree
Expand All @@ -141,13 +149,15 @@ def write_curve(self, xmlNode, curve, fill='none', stroke='#000000', width=2):
:type fill : String
:param stroke : Line color written in hex, i.e. '#000000'
:type stroke : String
:param opacity: Color opacity
:type opactiy: Float
:param width : Line width, measured in pixels
:type width : Int
:returns: None
:rtype : NoneType
"""
curveNode = etree.SubElement(xmlNode, 'path')
curveNode.attrib['style'] = 'fill:%s;stroke:%s;stroke-width:%dpx;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1' %(fill,stroke,width)
curveNode.attrib['style'] = f'fill:{fill};stroke:{stroke};stroke-width:{width}px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:{opacity}'
bezier = bezier_representation(curve)
bezier -= self.center
bezier *= self.scale
Expand All @@ -159,16 +169,22 @@ def write_curve(self, xmlNode, curve, fill='none', stroke='#000000', width=2):

curveNode.attrib['d'] = pathString

def write_surface(self, surface, fill='#ffcc99'):
def write_surface(self, surface, fill=None, subdivide=0):
""" Writes a Surface to the xml tree. This will draw the surface along with all knot lines

:param surface: The spline surface to write
:type surface: Surface
:param fill : Surface color written in hex, i.e. '#ffcc99'
:type fill : String
:param surface : The spline surface to write
:type surface : Surface
:param fill : Surface color written in hex, i.e. '#ffcc99'
:type fill : String
:param subdivide : Number of subdivision meshlines (inside elements) to be drawn on mesh
:type subdivide : int
:returns: None
:rtype : NoneType
"""

if fill is None:
fill = self.next_color()

# fetch boundary curves and create a connected, oriented bezier loop from it
bndry_curves = surface.edges()
bndry_curves[0].reverse()
Expand All @@ -179,25 +195,38 @@ def write_surface(self, surface, fill='#ffcc99'):
boundary.append(bndry_curves[3])

# fetch all meshlines (i.e. elements, also known as knot spans)
knot = surface.knots()
knot = surface.knots(with_multiplicities=False)
knotlines = []
for k in knot[0][1:-1]:
knotlines.append(surface.const_par_curve(k, 0))
for k in knot[1][1:-1]:
knotlines.append(surface.const_par_curve(k, 1))

# fetch all subdivison lines (i.e. inter-element evaluation points)
sublines = []
for i in range(len(knot[0])-1):
for k in np.linspace(knot[0][i], knot[0][i+1], subdivide, endpoint=False):
sublines.append(surface.const_par_curve(k, 0))
for i in range(len(knot[1])-1):
for k in np.linspace(knot[1][i], knot[1][i+1], subdivide, endpoint=False):
sublines.append(surface.const_par_curve(k, 1))

# create a group node for all elements corresponding to this surface patch
groupNode = etree.SubElement(self.xmlRoot, 'g')

# fill interior with a peach color
self.write_curve(groupNode, boundary, fill, width=2)
self.write_curve(groupNode, boundary, fill, width=3)

# draw all subgrid meshlines
for meshline in sublines:
self.write_curve(groupNode, meshline, width=1, stroke='#000000', opacity=0.3)

# draw all meshlines
for meshline in knotlines:
self.write_curve(groupNode, meshline, width=1)


def write(self, obj):
def write(self, obj, **kwargs):
""" Writes a list of planar curves and surfaces to vector graphics SVG file.
The image will never be stretched, and the geometry will keep width/height ratio
of original geometry, regardless of provided width/height ratio from arguments.
Expand All @@ -212,14 +241,15 @@ def write(self, obj):

if isinstance(obj[0], SplineObject): # input SplineModel or list
for o in obj:
self.write(o)
self.write(o, **kwargs)
return

if obj.dimension != 2:
raise RuntimeError('SVG files only applicable for 2D geometries')

# have to clone stuff we put here, in case they change on the outside
self.all_objects.append( obj.clone() )
self.all_kwargs.append( kwargs )

def read(self):
tree = etree.parse(self.filename)
Expand Down