def add(self, svg: Drawing) -> None: circle = svg.circle(to_grid(self.point), self.radius, stroke_width=0.5, fill="none", stroke="black") if not self.is_feasible: circle.update({"stroke-dasharray": "1,1"}) svg.add(circle) if self.is_terminal: circle = svg.circle(to_grid(self.point), self.radius - 1.0, fill="none", stroke="black") circle.update({"stroke-width": 0.5}) if not self.is_feasible: circle.update({"stroke-dasharray": "1,1"}) svg.add(circle) text_wrap = svg.text("", np.array((0, 2)) + to_grid(self.point), font_size="10px", font_family=FONT, text_anchor="middle") text_wrap.add(svg.tspan(self.name, font_style="italic")) text_wrap.add( svg.tspan(self.index, font_size="65%", baseline_shift="sub")) svg.add(text_wrap)
def _draw_rings(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, center: XY, radius_range: ValueRange) -> None: length_range = self.poster.length_range_by_date if not length_range.is_valid(): return min_length = length_range.lower() max_length = length_range.upper() assert min_length is not None assert max_length is not None ring_distance = self._determine_ring_distance(max_length) if ring_distance is None: return distance = ring_distance while distance < max_length: radius = radius_range.interpolate( (distance / max_length).magnitude) g.add( dr.circle( center=center.tuple(), r=radius, stroke=self._ring_color, stroke_opacity="0.2", fill="none", stroke_width=0.3, )) distance += ring_distance
def _draw_gate_circle(drawing: Drawing, x_coord: float, y_coord: float) -> None: drawing.add(drawing.circle(center=(x_coord, y_coord), r=_constants.GATE_SIZE/2, fill=_constants.GATE_FILL_COLOR, stroke=_constants.GATE_BORDER_COLOR, stroke_width=_constants.STROKE_THICKNESS))
def draw_svg(self, drawing: svgwrite.Drawing, g): glane = drawing.g() glane.attribs["class"] = "lane" cp1 = self.control_points[0] cp2 = self.control_points[-1] rel = relative_pose(cp1.as_SE2(), cp2.as_SE2()) _, theta = geo.translation_angle_from_SE2(rel) delta = int(np.sign(theta)) fill = {-1: "#577094", 0: "#709457", 1: "#947057"}[delta] points = self.lane_profile(points_per_segment=10) p = drawing.polygon(points=points, fill=fill, fill_opacity=0.5) glane.add(p) center_points = self.center_line_points(points_per_segment=10) center_points = [ geo.translation_angle_from_SE2(_)[0] for _ in center_points ] p = drawing.polyline( points=center_points, stroke=fill, fill="none", stroke_dasharray="0.02", stroke_width=0.01, ) glane.add(p) g.add(glane) for x in self.control_points: q = x.asmatrix2d().m p1, _ = geo.translation_angle_from_SE2(q) delta_arrow = np.array([self.width / 4, 0]) p2 = SE2_apply_R2(q, delta_arrow) gp = drawing.g() gp.attribs["class"] = "control-point" l = drawing.line( start=p1.tolist(), end=p2.tolist(), stroke="black", stroke_width=self.width / 20.0, ) gp.add(l) c = drawing.circle( center=p1.tolist(), r=self.width / 8, fill="white", stroke="black", stroke_width=self.width / 20.0, ) gp.add(c) g.add(gp)
def _draw_rings(self, d: svgwrite.Drawing, center: XY, radius_range: ValueRange): length_range = self.poster.length_range_by_date ring_distance = self._determine_ring_distance() if ring_distance is None: return distance = ring_distance while distance < length_range.upper(): radius = radius_range.lower() + radius_range.diameter() * distance / length_range.upper() d.add(d.circle(center=center.tuple(), r=radius, stroke=self._ring_color, stroke_opacity='0.2', fill='none', stroke_width=0.3)) distance += ring_distance
def _draw_control_circle(drawing: Drawing, x_coord: float, y_coord: float, desired_value: bool) -> None: if desired_value: filling_color = _constants.CONTROL_TRUE_GATE_FILL_COLOR else: filling_color = _constants.CONTROL_FALSE_GATE_FILL_COLOR drawing.add(drawing.circle(center=(x_coord, y_coord), r=_constants.CONTROL_GATE_SIZE / 2, fill=filling_color, stroke=_constants.GATE_BORDER_COLOR, stroke_width=_constants.STROKE_THICKNESS))
def render(self, dwg: Drawing) -> Group: g = dwg.g() eye = dwg.circle(center=(0, 0), r=self.radius, fill="white", stroke_width=0) g.add(eye) pupil_radius = self.radius * self.pupil_radius pupil_rho = min(self.radius * self.pupil_rho, self.radius * (1 - self.pupil_radius)) pupil_coords = (pupil_rho * math.cos(self.pupil_phi), pupil_rho * math.sin(self.pupil_phi)) pupil = dwg.circle(center=pupil_coords, r=pupil_radius, fill="black", stroke_width=0) g.add(pupil) reflection_radius = pupil_radius * .2 reflection_rho = pupil_radius * .6 reflection_phi = 1.75 * math.pi reflection_coords = (pupil_coords[0] + reflection_rho * math.cos(reflection_phi), pupil_coords[1] + reflection_rho * math.sin(reflection_phi)) reflection = dwg.circle(center=reflection_coords, r=reflection_radius, fill="white") g.add(reflection) return g
def render(self, dwg: Drawing) -> Group: r = dwg.rect(insert=(0, 0), size=(self.width, self.height), fill=self.color) g = dwg.g() g.add(r) star_count = int(self.width * self.height * .001) for i in range(star_count): s = dwg.circle(center=(self.prng.randint(0, self.width), self.prng.randint(0, self.height)), r=max(2, self.prng.gauss(1, 1)), fill="white") g.add(s) return g
def interpolate_poly_circle(svg: svgwrite.Drawing, poly, center, radius, t): center = Vec(*center) points = circle_points(center, radius, 40) result = [] for p in points: # svg.add(svg.circle(p.point2, 0.3, fill="magenta")) line = Line2D.from_points(center, p) q = poly_line_intersection(svg, poly, line) w = t * q + (1 - t) * p result.append(w) svg.add(svg.circle(q.point2, 0.3, fill="blue")) # svg.add(svg.line(center.point2, q.point2, stroke="grey", stroke_width=0.3)) return result
def display_svg(): dwg = Drawing() hlines = dwg.add(dwg.g(id="hlines", stroke="green")) for y in range(20): hlines.add( dwg.line(start=(2 * cm, (2 + y) * cm), end=(18 * cm, (2 + y) * cm))) vlines = dwg.add(dwg.g(id="vline", stroke="blue")) for x in range(17): vlines.add( dwg.line(start=((2 + x) * cm, 2 * cm), end=((2 + x) * cm, 21 * cm))) shapes = dwg.add(dwg.g(id="shapes", fill="red")) # set presentation attributes at object creation as SVG-Attributes circle = dwg.circle(center=(15 * cm, 8 * cm), r="2.5cm", stroke="blue", stroke_width=3) circle["class"] = "class1 class2" shapes.add(circle) # override the 'fill' attribute of the parent group 'shapes' shapes.add( dwg.rect( insert=(5 * cm, 5 * cm), size=(45 * mm, 45 * mm), fill="blue", stroke="red", stroke_width=3, )) # or set presentation attributes by helper functions of the Presentation-Mixin ellipse = shapes.add( dwg.ellipse(center=(10 * cm, 15 * cm), r=("5cm", "10mm"))) ellipse.fill("green", opacity=0.5).stroke("black", width=5).dasharray([20, 20]) return Response(dwg.tostring(), mimetype="image/svg+xml")
def QFN(name, offset, width1, width2, padh, padw, spaceing, padcount, bodyw, thermal): # document sizes doc = (width1 + 2 * offset[0], width1 + 2 * offset[1]) # basic SVG sheet svg = Drawing(filename=name, size=doc) ####################################################################################################### # PCB Pads ####################################################################################################### pads = svg.g(id="pads") for n in range(0, padcount): x = offset[0] + ((width1 - width2) / 2) + (n * spaceing) y = offset[1] pad = svg.rect(insert=(x, y), size=(padw, padh), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(pad) for n in range(0, padcount): x = offset[0] + ((width1 - width2) / 2) + (n * spaceing) y = offset[1] + (width1 - padh) pad = svg.rect(insert=(x, y), size=(padw, padh), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(pad) for n in range(0, padcount): x = offset[0] y = offset[1] + ((width1 - width2) / 2) + (n * spaceing) pad = svg.rect(insert=(x, y), size=(padh, padw), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(pad) for n in range(0, padcount): x = offset[0] + (width1 - padh) y = offset[1] + ((width1 - width2) / 2) + (n * spaceing) pad = svg.rect(insert=(x, y), size=(padh, padw), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(pad) # thermal pad t = svg.rect(insert=((doc[0] / 2) - (thermal / 2), (doc[1] / 2) - (thermal / 2)), size=(thermal, thermal), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(t) svg.add(pads) ####################################################################################################### # Part ####################################################################################################### corner = (doc[0] / 2) - (bodyw / 2) part = svg.g(id="part") body = svg.rect(insert=(corner, corner), size=(bodyw, bodyw), stroke_width=0.05, stroke="black", fill="#333333") part.add(body) # pin 1 indicator ind = svg.circle(center=(corner + 0.5, corner + bodyw - 0.5), r=0.2, stroke_width=0.05, stroke="black", fill="#333333") part.add(ind) svg.add(part) svg.save()
def plot(self, svg: Drawing) -> None: recolor: Optional[str] = None if isinstance(self.color, list): linear_gradient: LinearGradient = svg.linearGradient( self.map_((0, self.max_y)), self.map_((0, 1)), gradientUnits="userSpaceOnUse", ) for index, color in enumerate(self.color): linear_gradient.add_stop_color( index / (len(self.color) - 1), color.hex ) gradient: LinearGradient = svg.defs.add(linear_gradient) recolor = gradient.get_funciri() if isinstance(self.color, Color): recolor = self.color.hex svg.add( svg.rect( insert=(0, 0), size=("100%", "100%"), rx=None, ry=None, fill=self.background_color.hex, ) ) self.draw_grid(svg) last_text_y = 0 for xs, ys, color, title in self.data: if recolor: color = recolor assert len(xs) == len(ys) xs_second: list[float] = [ (x - self.min_x).total_seconds() for x in xs ] points = [] for index, x in enumerate(xs_second): y = ys[index] mapped: np.ndarray = map_array( np.array((x, y)), np.array((self.min_x_second, self.max_y)), np.array((self.max_x_second, self.min_y)), self.canvas.workspace[0], self.canvas.workspace[1], ) points.append(mapped) previous_point: Optional[np.ndarray] = None for point in points: if previous_point is not None: line: Line = svg.line( (previous_point[0], previous_point[1]), (point[0], point[1]), stroke=self.background_color.hex, stroke_width=6, ) svg.add(line) line: Line = svg.line( (previous_point[0], previous_point[1]), (point[0], point[1]), stroke=color, stroke_width=2, ) svg.add(line) previous_point = point for point in points: svg.add( svg.circle( (point[0], point[1]), 5.5, fill=self.background_color.hex, ) ) svg.add(svg.circle((point[0], point[1]), 3.5, fill=color)) title: str text_y = max(last_text_y + 20, point[1] + 6) self.text(svg, (point[0] + 15, text_y), title.upper(), color) last_text_y = text_y with Path(svg.filename).open("w+") as output_file: svg.write(output_file)
def LQFP(name, offset, width1, width2, padh, padw, spaceing, padcount, bodyw, edge, pinh, pinw): # document sizes doc = (width1 + 2 * offset[0], width1 + 2 * offset[1]) # basic SVG sheet svg = Drawing(filename=name, size=doc) ####################################################################################################### # PCB Pads ####################################################################################################### pads = svg.g(id="pads") # pins 51 to 75 for n in range(0, padcount): x = offset[0] + ((width1 - width2) / 2) + (n * spaceing) y = offset[1] pad = svg.rect(insert=(x, y), size=(padw, padh), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(pad) # pins 1 to 25 for n in range(0, padcount): x = offset[0] + ((width1 - width2) / 2) + (n * spaceing) y = offset[1] + (width1 - padh) pad = svg.rect(insert=(x, y), size=(padw, padh), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(pad) # pins 76 to 100 for n in range(0, padcount): x = offset[0] y = offset[1] + ((width1 - width2) / 2) + (n * spaceing) pad = svg.rect(insert=(x, y), size=(padh, padw), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(pad) # pins 26 to 50 for n in range(0, padcount): x = offset[0] + (width1 - padh) y = offset[1] + ((width1 - width2) / 2) + (n * spaceing) pad = svg.rect(insert=(x, y), size=(padh, padw), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(pad) svg.add(pads) ####################################################################################################### # Part ####################################################################################################### corner = (doc[0] / 2) - (bodyw / 2) part = svg.g(id="part") # pins 51 to 75 for n in range(0, padcount): x = offset[0] + ((width1 - width2) / 2) + ( (padw - pinw) / 2) + (n * spaceing) y = corner - pinh pad = svg.rect(insert=(x, y), size=(pinw, pinh), stroke_width=0.05, stroke="black", fill="#999999") part.add(pad) # pins 1 to 25 for n in range(0, padcount): x = offset[0] + ((width1 - width2) / 2) + ( (padw - pinw) / 2) + (n * spaceing) y = corner + bodyw pad = svg.rect(insert=(x, y), size=(pinw, pinh), stroke_width=0.05, stroke="black", fill="#999999") part.add(pad) # pins 76 to 100 for n in range(0, padcount): x = corner - pinh y = offset[0] + ((width1 - width2) / 2) + ( (padw - pinw) / 2) + (n * spaceing) pad = svg.rect(insert=(x, y), size=(pinh, pinw), stroke_width=0.05, stroke="black", fill="#999999") part.add(pad) # pins 26 to 50 for n in range(0, padcount): x = corner + bodyw y = offset[0] + ((width1 - width2) / 2) + ( (padw - pinw) / 2) + (n * spaceing) pad = svg.rect(insert=(x, y), size=(pinh, pinw), stroke_width=0.05, stroke="black", fill="#999999") part.add(pad) # plastic body points = [(corner, corner + edge), (corner + edge, corner), (corner + bodyw - edge, corner), (corner + bodyw, corner + edge), (corner + bodyw, corner + bodyw - edge), (corner + bodyw - edge, corner + bodyw), (corner + edge, corner + bodyw), (corner, corner + bodyw - edge)] body = svg.polygon(points=points, stroke_width=0.05, stroke="black", fill="#333333") part.add(body) # pin 1 indicator ind = svg.circle(center=(corner + 1, corner + bodyw - 1), r=0.5, stroke_width=0.05, stroke="black", fill="#333333") part.add(ind) svg.add(part) svg.save()
def disvg(paths=None, colors=None, filename=os_path.join(getcwd(), 'disvg_output.svg'), stroke_widths=None, nodes=None, node_colors=None, node_radii=None, openinbrowser=True, timestamp=False, margin_size=0.1, mindim=600, dimensions=None, viewbox=None, text=None, text_path=None, font_size=None, attributes=None, svg_attributes=None): """Takes in a list of paths and creates an SVG file containing said paths. REQUIRED INPUTS: :param paths - a list of paths OPTIONAL INPUT: :param colors - specifies the path stroke color. By default all paths will be black (#000000). This paramater can be input in a few ways 1) a list of strings that will be input into the path elements stroke attribute (so anything that is understood by the svg viewer). 2) a string of single character colors -- e.g. setting colors='rrr' is equivalent to setting colors=['red', 'red', 'red'] (see the 'color_dict' dictionary above for a list of possibilities). 3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...]. :param filename - the desired location/filename of the SVG file created (by default the SVG will be stored in the current working directory and named 'disvg_output.svg'). :param stroke_widths - a list of stroke_widths to use for paths (default is 0.5% of the SVG's width or length) :param nodes - a list of points to draw as filled-in circles :param node_colors - a list of colors to use for the nodes (by default nodes will be red) :param node_radii - a list of radii to use for the nodes (by default nodes will be radius will be 1 percent of the svg's width/length) :param text - string or list of strings to be displayed :param text_path - if text is a list, then this should be a list of path (or path segments of the same length. Note: the path must be long enough to display the text or the text will be cropped by the svg viewer. :param font_size - a single float of list of floats. :param openinbrowser - Set to True to automatically open the created SVG in the user's default web browser. :param timestamp - if True, then the a timestamp will be appended to the output SVG's filename. This will fix issues with rapidly opening multiple SVGs in your browser. :param margin_size - The min margin (empty area framing the collection of paths) size used for creating the canvas and background of the SVG. :param mindim - The minimum dimension (height or width) of the output SVG (default is 600). :param dimensions - The display dimensions of the output SVG. Using this will override the mindim parameter. :param viewbox - This specifies what rectangular patch of R^2 will be viewable through the outputSVG. It should be input in the form (min_x, min_y, width, height). This is different from the display dimension of the svg, which can be set through mindim or dimensions. :param attributes - a list of dictionaries of attributes for the input paths. Note: This will override any other conflicting settings. :param svg_attributes - a dictionary of attributes for output svg. Note 1: This will override any other conflicting settings. Note 2: Setting `svg_attributes={'debug': False}` may result in a significant increase in speed. NOTES: -The unit of length here is assumed to be pixels in all variables. -If this function is used multiple times in quick succession to display multiple SVGs (all using the default filename), the svgviewer/browser will likely fail to load some of the SVGs in time. To fix this, use the timestamp attribute, or give the files unique names, or use a pause command (e.g. time.sleep(1)) between uses. """ _default_relative_node_radius = 5e-3 _default_relative_stroke_width = 1e-3 _default_path_color = '#000000' # black _default_node_color = '#ff0000' # red _default_font_size = 12 # append directory to filename (if not included) if os_path.dirname(filename) == '': filename = os_path.join(getcwd(), filename) # append time stamp to filename if timestamp: fbname, fext = os_path.splitext(filename) dirname = os_path.dirname(filename) tstamp = str(time()).replace('.', '') stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext filename = os_path.join(dirname, stfilename) # check paths and colors are set if isinstance(paths, Path) or is_path_segment(paths): paths = [paths] if paths: if not colors: colors = [_default_path_color] * len(paths) else: assert len(colors) == len(paths) if isinstance(colors, str): colors = str2colorlist(colors, default_color=_default_path_color) elif isinstance(colors, list): for idx, c in enumerate(colors): if is3tuple(c): colors[idx] = "rgb" + str(c) # check nodes and nodes_colors are set (node_radii are set later) if nodes: if not node_colors: node_colors = [_default_node_color] * len(nodes) else: assert len(node_colors) == len(nodes) if isinstance(node_colors, str): node_colors = str2colorlist(node_colors, default_color=_default_node_color) elif isinstance(node_colors, list): for idx, c in enumerate(node_colors): if is3tuple(c): node_colors[idx] = "rgb" + str(c) # set up the viewBox and display dimensions of the output SVG # along the way, set stroke_widths and node_radii if not provided assert paths or nodes stuff2bound = [] if viewbox: szx, szy = viewbox[2:4] else: if paths: stuff2bound += paths if nodes: stuff2bound += nodes if text_path: stuff2bound += text_path xmin, xmax, ymin, ymax = big_bounding_box(stuff2bound) dx = xmax - xmin dy = ymax - ymin if dx == 0: dx = 1 if dy == 0: dy = 1 # determine stroke_widths to use (if not provided) and max_stroke_width if paths: if not stroke_widths: sw = max(dx, dy) * _default_relative_stroke_width stroke_widths = [sw] * len(paths) max_stroke_width = sw else: assert len(paths) == len(stroke_widths) max_stroke_width = max(stroke_widths) else: max_stroke_width = 0 # determine node_radii to use (if not provided) and max_node_diameter if nodes: if not node_radii: r = max(dx, dy) * _default_relative_node_radius node_radii = [r] * len(nodes) max_node_diameter = 2 * r else: assert len(nodes) == len(node_radii) max_node_diameter = 2 * max(node_radii) else: max_node_diameter = 0 extra_space_for_style = max(max_stroke_width, max_node_diameter) xmin -= margin_size * dx + extra_space_for_style / 2 ymin -= margin_size * dy + extra_space_for_style / 2 dx += 2 * margin_size * dx + extra_space_for_style dy += 2 * margin_size * dy + extra_space_for_style viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy) if dimensions: szx, szy = dimensions else: if dx > dy: szx = str(mindim) + 'px' szy = str(int(ceil(mindim * dy / dx))) + 'px' else: szx = str(int(ceil(mindim * dx / dy))) + 'px' szy = str(mindim) + 'px' # Create an SVG file if svg_attributes: szx = svg_attributes.get("width", szx) szy = svg_attributes.get("height", szy) dwg = Drawing(filename=filename, size=(szx, szy), **svg_attributes) else: dwg = Drawing(filename=filename, size=(szx, szy), viewBox=viewbox) # add paths if paths: for i, p in enumerate(paths): if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p if attributes: good_attribs = {'d': ps} for key in attributes[i]: val = attributes[i][key] if key != 'd': try: dwg.path(ps, **{key: val}) good_attribs.update({key: val}) except Exception as e: warn(str(e)) dwg.add(dwg.path(**good_attribs)) else: dwg.add( dwg.path(ps, stroke=colors[i], stroke_width=str(stroke_widths[i]), fill='none')) # add nodes (filled in circles) if nodes: for i_pt, pt in enumerate([(z.real, z.imag) for z in nodes]): dwg.add(dwg.circle(pt, node_radii[i_pt], fill=node_colors[i_pt])) # add texts if text: assert isinstance(text, str) or (isinstance(text, list) and isinstance( text_path, list) and len(text_path) == len(text)) if isinstance(text, str): text = [text] if not font_size: font_size = [_default_font_size] if not text_path: pos = complex(xmin + margin_size * dx, ymin + margin_size * dy) text_path = [Line(pos, pos + 1).d()] else: if font_size: if isinstance(font_size, list): assert len(font_size) == len(text) else: font_size = [font_size] * len(text) else: font_size = [_default_font_size] * len(text) for idx, s in enumerate(text): p = text_path[idx] if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p # paragraph = dwg.add(dwg.g(font_size=font_size[idx])) # paragraph.add(dwg.textPath(ps, s)) pathid = 'tp' + str(idx) dwg.defs.add(dwg.path(d=ps, id=pathid)) txter = dwg.add(dwg.text('', font_size=font_size[idx])) txter.add(txt.TextPath('#' + pathid, s)) # save svg if not os_path.exists(os_path.dirname(filename)): makedirs(os_path.dirname(filename)) dwg.save() # re-open the svg, make the xml pretty, and save it again xmlstring = md_xml_parse(filename).toprettyxml() with open(filename, 'w') as f: f.write(xmlstring) # try to open in web browser if openinbrowser: try: open_in_browser(filename) except: print("Failed to open output SVG in browser. SVG saved to:") print(filename)
def add_graphelement_to_svg_drawing(element: GraphElement, drawing: svgwrite.Drawing, filters: Dict[str, Filter]) -> None: args = {} for attr, value in element.attr.items(): if attr.startswith('.svg_tag'): continue if attr.startswith('.svg_'): name = attr[5:] if name == 'filter': args[name] = filters[value].get_funciri() else: args[name] = value if '.svg_tag' in element.attr: tag = element.attr['.svg_tag'] if tag == 'rect': x = float(element.attr['x']) y = -float(element.attr['y']) width = float(element.attr.get('.svg_width', 0.1)) height = float(element.attr.get('.svg_height', 0.1)) x = x - width / 2 y = y - height / 2 drawing.add(drawing.rect((x*mult, y*mult), (width*mult, height*mult), **args)) elif tag == 'path': drawing.add(drawing.path(**args)) elif tag == 'circle': x = float(element.attr['x']) y = -float(element.attr['y']) args.setdefault('r', '1cm') args.setdefault('stroke_width', '0.1mm') args.setdefault('stroke', 'black') args.setdefault('fill', 'none') drawing.add(drawing.circle(center=(x * mult, y * mult), **args)) elif tag == 'image': x = float(element.attr['x']) y = -float(element.attr['y']) width = float(element.attr.pop('.svg_width', 5)) height = float(element.attr.pop('.svg_height', 5)) x = x - width / 2 y = y - height / 2 center = ((x + width / 2), (y + height / 2)) args.setdefault('insert', (x * mult, y * mult)) args.setdefault('size', (width * mult, height * mult)) if '.svgx_rotate' in element.attr: rotation = float(element.attr['.svgx_rotate']) args.setdefault('transform', f'translate({center[0]*mult}, {center[1]*mult}) ' f'rotate({-rotation}) ' f'translate({-center[0]*mult}, {-center[1]*mult})' ) drawing.add(getattr(drawing, element.attr['.svg_tag'])(**args)) elif tag != 'None' and tag is not None: drawing.add(getattr(drawing, element.attr['.svg_tag'])(**args)) elif isinstance(element, Vertex): if '.helper_node' in element.attr and element.attr['.helper_node']: return x = float(element.attr['x']) y = -float(element.attr['y']) args.setdefault('r', '0.4cm') args.setdefault('stroke_width', '1mm') args.setdefault('stroke', 'black') args.setdefault('fill', 'none') drawing.add(drawing.circle(center=(x*mult, y*mult), **args)) elif isinstance(element, Edge): v1 = element.vertex1 v2 = element.vertex2 x1 = float(v1.attr['x']) y1 = -float(v1.attr['y']) x2 = float(v2.attr['x']) y2 = -float(v2.attr['y']) args.setdefault('stroke_width', '1mm') args.setdefault('stroke', 'black') drawing.add(drawing.line(start=(x1*mult, y1*mult), end=(x2*mult, y2*mult), **args)) else: raise ValueError
def export_node(node, store, size=None): """Construct a SVG description for a workflow node. Args: node (NodeDef) store (dict of uid, def): elements definitions size (int, int): size of drawing in pixels Returns: (str) - SVG description of workflow node """ pfs = port_font_size # node size pr = port_radius pspace = pr * 9 nw = compute_node_width(node, node['name'], pspace) nh = label_font_size + 2 * pr + 2 * pfs + 2 + (2 * node_padding) # draw if size is None: size = (600, 600) paper = Drawing("workflow_node.svg", size, id="repr") lg = paper.linearGradient((0.5, 0), (0.5, 1.), id="in_port") lg.add_stop_color(0, color='#3333ff') lg.add_stop_color(1, color='#2222ff') paper.defs.add(lg) lg = paper.linearGradient((0.5, 0), (0.5, 1.), id="out_port") lg.add_stop_color(0, color='#ffff33') lg.add_stop_color(1, color='#9a9a00') paper.defs.add(lg) # body g = paper.add(paper.g()) # background lg = paper.linearGradient((0.5, 0), (0.5, 1.)) lg.add_stop_color(0, color='#8c8cff') lg.add_stop_color(1, color='#c8c8c8') paper.defs.add(lg) bg = paper.rect((-nw / 2, -nh / 2), (nw, nh), rx=node_padding, ry=node_padding, stroke_width=1) bg.stroke('#808080') bg.fill(lg) g.add(bg) # label style = ('font-size: %dpx; font-family: %s; ' 'text-anchor: middle' % (label_font_size, label_font)) frag = paper.tspan(node['name'], dy=[label_font_size // 3]) label = paper.text("", style=style, fill='#000000') label.add(frag) g.add(label) # ports port_style = ('font-size: %dpx; ' % pfs + 'font-family: %s; ' % label_font) onstyle = port_style + 'text-anchor: end' instyle = port_style + 'text-anchor: start' istyle = port_style + 'text-anchor: middle' nb = len(node['inputs']) py = -nh / 2 for i, pdef in enumerate(node['inputs']): px = i * pspace - pspace * (nb - 1) / 2 pg = g.add(paper.g()) pg.translate(px, py) idef = store.get(pdef['interface'], None) if idef is not None and 'url' in idef: link = pg.add(paper.a(href=idef['url'], target='_top')) else: link = pg port = paper.circle((0, 0), pr, stroke='#000000', stroke_width=1) port.fill("url(#in_port)") link.add(port) # port name frag = paper.tspan(pdef['name'], dy=[-2 * pr]) label = paper.text("", style=instyle, fill='#000000') label.rotate(-45) label.add(frag) pg.add(label) # port interface if idef is None: itxt = pdef['interface'] else: itxt = idef['name'] if len(itxt) > 10: itxt = itxt[:7] + "..." frag = paper.tspan(itxt, dy=[pr + pfs]) label = paper.text("", style=istyle, fill='#000000') label.add(frag) link.add(label) nb = len(node['outputs']) py = nh / 2 for i, pdef in enumerate(node['outputs']): px = i * pspace - pspace * (nb - 1) / 2 pg = g.add(paper.g()) pg.translate(px, py) idef = store.get(pdef['interface'], None) if idef is not None and 'url' in idef: link = pg.add(paper.a(href=idef['url'], target='_top')) else: link = pg port = paper.circle((0, 0), pr, stroke='#000000', stroke_width=1) port.fill("url(#out_port)") link.add(port) # port name frag = paper.tspan(pdef['name'], dy=[2 * pr + pfs // 2]) label = paper.text("", style=onstyle, fill='#000000') label.rotate(-45) label.add(frag) pg.add(label) # port interface if idef is None: itxt = pdef['interface'] else: itxt = idef['name'] if len(itxt) > 10: itxt = itxt[:7] + "..." frag = paper.tspan(itxt, dy=[- pr - 2]) label = paper.text("", style=istyle, fill='#000000') label.add(frag) link.add(label) # reformat whole drawing to fit screen xmin = - nw / 2 - draw_padding / 10. xmax = + nw / 2 + draw_padding / 10. if len(node['inputs']) == 0: inames_extend = 0 else: inames = [(len(pdef['name']), pdef['name']) for pdef in node['inputs']] inames_extend = string_size(sorted(inames)[-1][1], pfs) * 0.7 + pfs ymin = - nh / 2 - pr - inames_extend - draw_padding / 10. if len(node['outputs']) == 0: onames_extend = 0 else: onames = [(len(pdef['name']), pdef['name']) for pdef in node['outputs']] onames_extend = string_size(sorted(onames)[-1][1], pfs) * 0.7 + pfs ymax = + nh / 2 + pr + onames_extend + draw_padding / 10. w = float(size[0]) h = float(size[1]) ratio = max((xmax - xmin) / w, (ymax - ymin) / h) xsize = ratio * w ysize = ratio * h bb = (xmin * xsize / (xmax - xmin), ymin * ysize / (ymax - ymin), xsize, ysize) paper.viewbox(*bb) return paper.tostring(), bb
def disvg(paths=None, colors=None, filename=os_path.join(getcwd(), 'disvg_output.svg'), stroke_widths=None, nodes=None, node_colors=None, node_radii=None, openinbrowser=True, timestamp=False, margin_size=0.1, mindim=600, dimensions=None, viewbox=None, text=None, text_path=None, font_size=None, attributes=None, svg_attributes=None, svgwrite_debug=False, paths2Drawing=False): """Takes in a list of paths and creates an SVG file containing said paths. REQUIRED INPUTS: :param paths - a list of paths OPTIONAL INPUT: :param colors - specifies the path stroke color. By default all paths will be black (#000000). This paramater can be input in a few ways 1) a list of strings that will be input into the path elements stroke attribute (so anything that is understood by the svg viewer). 2) a string of single character colors -- e.g. setting colors='rrr' is equivalent to setting colors=['red', 'red', 'red'] (see the 'color_dict' dictionary above for a list of possibilities). 3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...]. :param filename - the desired location/filename of the SVG file created (by default the SVG will be stored in the current working directory and named 'disvg_output.svg'). :param stroke_widths - a list of stroke_widths to use for paths (default is 0.5% of the SVG's width or length) :param nodes - a list of points to draw as filled-in circles :param node_colors - a list of colors to use for the nodes (by default nodes will be red) :param node_radii - a list of radii to use for the nodes (by default nodes will be radius will be 1 percent of the svg's width/length) :param text - string or list of strings to be displayed :param text_path - if text is a list, then this should be a list of path (or path segments of the same length. Note: the path must be long enough to display the text or the text will be cropped by the svg viewer. :param font_size - a single float of list of floats. :param openinbrowser - Set to True to automatically open the created SVG in the user's default web browser. :param timestamp - if True, then the a timestamp will be appended to the output SVG's filename. This will fix issues with rapidly opening multiple SVGs in your browser. :param margin_size - The min margin (empty area framing the collection of paths) size used for creating the canvas and background of the SVG. :param mindim - The minimum dimension (height or width) of the output SVG (default is 600). :param dimensions - The (x,y) display dimensions of the output SVG. I.e. this specifies the `width` and `height` SVG attributes. Note that these also can be used to specify units other than pixels. Using this will override the `mindim` parameter. :param viewbox - This specifies the coordinated system used in the svg. The SVG `viewBox` attribute works together with the the `height` and `width` attrinutes. Using these three attributes allows for shifting and scaling of the SVG canvas without changing the any values other than those in `viewBox`, `height`, and `width`. `viewbox` should be input as a 4-tuple, (min_x, min_y, width, height), or a string "min_x min_y width height". Using this will override the `mindim` parameter. :param attributes - a list of dictionaries of attributes for the input paths. Note: This will override any other conflicting settings. :param svg_attributes - a dictionary of attributes for output svg. :param svgwrite_debug - This parameter turns on/off `svgwrite`'s debugging mode. By default svgwrite_debug=False. This increases speed and also prevents `svgwrite` from raising of an error when not all `svg_attributes` key-value pairs are understood. :param paths2Drawing - If true, an `svgwrite.Drawing` object is returned and no file is written. This `Drawing` can later be saved using the `svgwrite.Drawing.save()` method. NOTES: * The `svg_attributes` parameter will override any other conflicting settings. * Any `extra` parameters that `svgwrite.Drawing()` accepts can be controlled by passing them in through `svg_attributes`. * The unit of length here is assumed to be pixels in all variables. * If this function is used multiple times in quick succession to display multiple SVGs (all using the default filename), the svgviewer/browser will likely fail to load some of the SVGs in time. To fix this, use the timestamp attribute, or give the files unique names, or use a pause command (e.g. time.sleep(1)) between uses. """ _default_relative_node_radius = 5e-3 _default_relative_stroke_width = 1e-3 _default_path_color = '#000000' # black _default_node_color = '#ff0000' # red _default_font_size = 12 # append directory to filename (if not included) if os_path.dirname(filename) == '': filename = os_path.join(getcwd(), filename) # append time stamp to filename if timestamp: fbname, fext = os_path.splitext(filename) dirname = os_path.dirname(filename) tstamp = str(time()).replace('.', '') stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext filename = os_path.join(dirname, stfilename) # check paths and colors are set if isinstance(paths, Path) or is_path_segment(paths): paths = [paths] if paths: if not colors: colors = [_default_path_color] * len(paths) else: assert len(colors) == len(paths) if isinstance(colors, str): colors = str2colorlist(colors, default_color=_default_path_color) elif isinstance(colors, list): for idx, c in enumerate(colors): if is3tuple(c): colors[idx] = "rgb" + str(c) # check nodes and nodes_colors are set (node_radii are set later) if nodes: if not node_colors: node_colors = [_default_node_color] * len(nodes) else: assert len(node_colors) == len(nodes) if isinstance(node_colors, str): node_colors = str2colorlist(node_colors, default_color=_default_node_color) elif isinstance(node_colors, list): for idx, c in enumerate(node_colors): if is3tuple(c): node_colors[idx] = "rgb" + str(c) # set up the viewBox and display dimensions of the output SVG # along the way, set stroke_widths and node_radii if not provided assert paths or nodes stuff2bound = [] if viewbox: if not isinstance(viewbox, str): viewbox = '%s %s %s %s' % viewbox if dimensions is None: dimensions = viewbox.split(' ')[2:4] elif dimensions: dimensions = tuple(map(str, dimensions)) def strip_units(s): return re.search(r'\d*\.?\d*', s.strip()).group() viewbox = '0 0 %s %s' % tuple(map(strip_units, dimensions)) else: if paths: stuff2bound += paths if nodes: stuff2bound += nodes if text_path: stuff2bound += text_path xmin, xmax, ymin, ymax = big_bounding_box(stuff2bound) dx = xmax - xmin dy = ymax - ymin if dx == 0: dx = 1 if dy == 0: dy = 1 # determine stroke_widths to use (if not provided) and max_stroke_width if paths: if not stroke_widths: sw = max(dx, dy) * _default_relative_stroke_width stroke_widths = [sw]*len(paths) max_stroke_width = sw else: assert len(paths) == len(stroke_widths) max_stroke_width = max(stroke_widths) else: max_stroke_width = 0 # determine node_radii to use (if not provided) and max_node_diameter if nodes: if not node_radii: r = max(dx, dy) * _default_relative_node_radius node_radii = [r]*len(nodes) max_node_diameter = 2*r else: assert len(nodes) == len(node_radii) max_node_diameter = 2*max(node_radii) else: max_node_diameter = 0 extra_space_for_style = max(max_stroke_width, max_node_diameter) xmin -= margin_size*dx + extra_space_for_style/2 ymin -= margin_size*dy + extra_space_for_style/2 dx += 2*margin_size*dx + extra_space_for_style dy += 2*margin_size*dy + extra_space_for_style viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy) if dx > dy: szx = str(mindim) + 'px' szy = str(int(ceil(mindim * dy / dx))) + 'px' else: szx = str(int(ceil(mindim * dx / dy))) + 'px' szy = str(mindim) + 'px' dimensions = szx, szy # Create an SVG file if svg_attributes is not None: dimensions = (svg_attributes.get("width", dimensions[0]), svg_attributes.get("height", dimensions[1])) debug = svg_attributes.get("debug", svgwrite_debug) dwg = Drawing(filename=filename, size=dimensions, debug=debug, **svg_attributes) else: dwg = Drawing(filename=filename, size=dimensions, debug=svgwrite_debug, viewBox=viewbox) # add paths if paths: for i, p in enumerate(paths): if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p if attributes: good_attribs = {'d': ps} for key in attributes[i]: val = attributes[i][key] if key != 'd': try: dwg.path(ps, **{key: val}) good_attribs.update({key: val}) except Exception as e: warn(str(e)) dwg.add(dwg.path(**good_attribs)) else: dwg.add(dwg.path(ps, stroke=colors[i], stroke_width=str(stroke_widths[i]), fill='none')) # add nodes (filled in circles) if nodes: for i_pt, pt in enumerate([(z.real, z.imag) for z in nodes]): dwg.add(dwg.circle(pt, node_radii[i_pt], fill=node_colors[i_pt])) # add texts if text: assert isinstance(text, str) or (isinstance(text, list) and isinstance(text_path, list) and len(text_path) == len(text)) if isinstance(text, str): text = [text] if not font_size: font_size = [_default_font_size] if not text_path: pos = complex(xmin + margin_size*dx, ymin + margin_size*dy) text_path = [Line(pos, pos + 1).d()] else: if font_size: if isinstance(font_size, list): assert len(font_size) == len(text) else: font_size = [font_size] * len(text) else: font_size = [_default_font_size] * len(text) for idx, s in enumerate(text): p = text_path[idx] if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p # paragraph = dwg.add(dwg.g(font_size=font_size[idx])) # paragraph.add(dwg.textPath(ps, s)) pathid = 'tp' + str(idx) dwg.defs.add(dwg.path(d=ps, id=pathid)) txter = dwg.add(dwg.text('', font_size=font_size[idx])) txter.add(txt.TextPath('#'+pathid, s)) if paths2Drawing: return dwg # save svg if not os_path.exists(os_path.dirname(filename)): makedirs(os_path.dirname(filename)) dwg.save() # re-open the svg, make the xml pretty, and save it again xmlstring = md_xml_parse(filename).toprettyxml() with open(filename, 'w') as f: f.write(xmlstring) # try to open in web browser if openinbrowser: try: open_in_browser(filename) except: print("Failed to open output SVG in browser. SVG saved to:") print(filename)
def render(self, dwg: Drawing) -> Group: g = dwg.g() # Render antennae antennae_rho = self.size * self.antennae_rho antennae_base_phi = 1.5 * math.pi antennae_delta_phi = 0.2 * math.pi antennae_delta_phi_total = (self.antennae_count - 1) * antennae_delta_phi antennae_left_phi = antennae_base_phi - antennae_delta_phi_total / 2 for i in range(self.antennae_count): antennae_coords = ( antennae_rho * math.cos(antennae_left_phi + i * antennae_delta_phi), antennae_rho * math.sin(antennae_left_phi + i * antennae_delta_phi)) antenna = dwg.circle(center=antennae_coords, r=self.size * self.antennae_relative_size, fill=self.color) path = dwg.path(stroke=helper.colors.darken_hex(self.color), stroke_width=3) path.push("M %f %f" % antennae_coords) path.push("L %f %f" % (0, 0)) g.add(path) g.add(antenna) # Render body shadow_mask = dwg.defs.add(dwg.clipPath(id="bodymask-%s" % self.id)) shadow_mask.add(dwg.circle(center=(0, 0), r=self.size)) body_dark = dwg.circle(center=(0, 0), r=self.size, stroke_width=0, fill=helper.colors.darken_hex(self.color), clip_path="url(#bodymask-%s)" % self.id) g.add(body_dark) body_highlight = dwg.circle(center=(-self.size * .2, -self.size * .2), r=self.size * .95, stroke_width=0, fill=self.color, clip_path="url(#bodymask-%s)" % self.id) g.add(body_highlight) # Render eyes eye_rho = self.size * self.eye_distance eye_base_phi = 1.5 * math.pi eye_delta_phi = 0.3 * math.pi eye_delta_phi_total = (self.eye_count - 1) * eye_delta_phi eye_left_phi = eye_base_phi - eye_delta_phi_total / 2 for eye_i in range(self.eye_count): eye_coords = (eye_rho * math.cos(eye_left_phi + eye_i * eye_delta_phi), eye_rho * math.sin(eye_left_phi + eye_i * eye_delta_phi)) eye = self.eye_factory(self.size * self.eye_relative_size) eye_g = eye.render(dwg) eye_g.translate(eye_coords[0], eye_coords[1]) g.add(eye_g) # Render mouth mouth = self.mouth_factory(self.size * self.mouth_relative_size, helper.colors.darken_hex(self.color, .75)) mouth_g = mouth.render(dwg) mouth_g.translate(0, self.size * self.mouth_height) g.add(mouth_g) return g
def render(self, dwg: Drawing) -> Group: g = dwg.g() head = self.head.render(dwg) socket_relative_width = 1.2 socket_radius = (self.head_size * socket_relative_width, self.head_size*0.3) socket_relative_height = .3 socket_left = (-self.head_size * socket_relative_width, self.head_size * .5) socket_left_bottom = (socket_left[0], socket_left[1] + self.head_size * socket_relative_height) socket_right: Tuple[float, float] = (self.head_size * socket_relative_width, self.head_size * .5) socket_right_bottom: Tuple[float, float] = (socket_right[0], socket_right[1] + self.head_size * socket_relative_height) size_factor = self.head_size / 50.0 arm_length = 50 * size_factor arm_params = { "arm_length": arm_length, "arm_color": self.body_color, "hand_color": helper.colors.lighten_hex(self.body_color, 2), "thickness_shoulder": 30 * size_factor } arm_params.update(self.arm_params) for i in range(self.arm_count): left_arm = ArmWithHand(**arm_params) # type: ignore left_arm_g = left_arm.render(dwg) left_arm_x = socket_right_bottom[0] - left_arm.thickness_shoulder / 2 - (socket_right_bottom[0] - self.head_size * self.body_fatness) / (self.head_size * self.body_height) * i * left_arm.thickness_shoulder * 1.2 left_arm_g.translate(left_arm_x, socket_right_bottom[1] + left_arm.thickness_shoulder / 2 + i * left_arm.thickness_shoulder * .8) left_arm_g.rotate(self.body_left_arm_angle / (math.pi) * 180 + (i * 20)) g.add(left_arm_g) right_arm = ArmWithHand(reverse_shadow=True, **arm_params) # type: ignore right_arm_g = right_arm.render(dwg) right_arm_x = socket_left_bottom[0] + right_arm.thickness_shoulder / 2 + (-self.head_size * self.body_fatness - socket_left_bottom[0]) / (self.head_size * self.body_height) * i * right_arm.thickness_shoulder * 1.2 right_arm_g.translate(right_arm_x, socket_left_bottom[1] + right_arm.thickness_shoulder / 2 + i * right_arm.thickness_shoulder * .8) right_arm_g.rotate(-self.body_right_arm_angle / (math.pi) * 180 - (i * 20)) right_arm_g.scale(-1, 1) g.add(right_arm_g) leg_thickness_thigh = self.body_fatness * self.head_size leg_thickness_foot = leg_thickness_thigh * .7 leg_length = self.head_size * 1 boot_height = leg_length * .5 foot_length = leg_length left_leg = LegWithFoot(leg_length=leg_length, # type: ignore leg_color=self.body_color, thickness_thigh=leg_thickness_thigh, thickness_foot=leg_thickness_foot, foot_color=helper.colors.lighten_hex(self.body_color, 2), boot_height=boot_height, foot_length=foot_length, **self.leg_params) left_leg_g = left_leg.render(dwg) left_leg_g.translate(0, self.head_size * self.body_height) left_leg_g.rotate(-20) g.add(left_leg_g) right_leg = LegWithFoot(leg_length=leg_length, # type: ignore leg_color=self.body_color, thickness_thigh=leg_thickness_thigh, thickness_foot=leg_thickness_foot, foot_color=helper.colors.lighten_hex(self.body_color, 2), boot_height=boot_height, foot_length=foot_length, **self.leg_params) right_leg_g = right_leg.render(dwg) right_leg_g.translate(0, self.head_size * self.body_height) right_leg_g.rotate(20) right_leg_g.scale(-1, 1) g.add(right_leg_g) body = dwg.path(fill=self.body_color) body.push("M %f %f" % (socket_right_bottom[0], socket_right_bottom[1])) body.push("L %f %f" % (self.head_size * self.body_fatness, self.head_size * self.body_height)) body.push("L %f %f" % (self.head_size * (self.body_fatness - .2), self.head_size * (self.body_height + .2))) body.push("L %f %f" % (-self.head_size * (self.body_fatness - .2), self.head_size * (self.body_height + .2))) body.push("L %f %f" % (-self.head_size * self.body_fatness, self.head_size * self.body_height)) body.push("L %f %f" % (socket_left_bottom[0], socket_left_bottom[1])) g.add(body) socket_background_color = helper.colors.darken_hex(self.socket_color) socket_background = dwg.ellipse(fill=socket_background_color, center=(0, self.head_size * .5), r=socket_radius) socket_foreground = dwg.path(fill=self.socket_color) socket_foreground.push("M %f %f" % socket_left) socket_foreground.push("A %f %f 0 0 0 %f %f" % (socket_radius[0], socket_radius[1], socket_right[0], socket_right[1])) socket_foreground.push("l 0 %f" % (self.head_size * .3)) socket_foreground.push("A %f %f 0 0 1 %f %f" % (socket_radius[0], socket_radius[1], - self.head_size * socket_relative_width, self.head_size * .8)) g.add(socket_background) g.add(head) g.add(socket_foreground) dome = dwg.path(fill="white", fill_opacity=.3) dome.push("M %f %f" % socket_left) dome.push("C %f %f %f %f %f %f" % (-self.head_size * (socket_relative_width + 1), -self.head_size * 3, self.head_size * (socket_relative_width + 1), -self.head_size * 3, socket_right[0], socket_right[1])) dome.push("A %f %f 0 0 1 %f %f" % (socket_radius[0], socket_radius[1], socket_left[0], socket_left[1])) refl_mask = dwg.defs.add(dwg.mask((self.head_size * -1.5, self.head_size * -2.5), (self.head_size * 3, self.head_size * 5), id="%s-dome-refl-mask" % self.id)) refl_mask.add(dwg.rect((self.head_size * -1.5, self.head_size * -2.5), (self.head_size * 3, self.head_size * 5), fill="white")) refl_mask.add(dwg.circle((self.head_size * .3, -self.head_size * .25), r=self.head_size * 1.75, fill="black")) dome_reflection = dwg.path(fill="white", fill_opacity=.3, mask="url(#%s-dome-refl-mask)" % self.id) dome_reflection.push("M %f %f" % socket_left) dome_reflection.push("C %f %f %f %f %f %f" % (-self.head_size * (socket_relative_width + 1), -self.head_size * 3, self.head_size * (socket_relative_width + 1), -self.head_size * 3, socket_right[0], socket_right[1])) dome_reflection.push("A %f %f 0 0 1 %f %f" % (socket_radius[0], socket_radius[1], socket_left[0], socket_left[1])) dome_reflection.scale(.9) g.add(dome) g.add(dome_reflection) return g
def export_node(node, store, size=None): """Construct a SVG description for a workflow node. Args: node (NodeDef) store (dict of uid, def): elements definitions size (int, int): size of drawing in pixels Returns: (str) - SVG description of workflow node """ pfs = port_font_size # node size pr = port_radius pspace = pr * 9 nw = compute_node_width(node, node['name'], pspace) nh = label_font_size + 2 * pr + 2 * pfs + 2 + (2 * node_padding) # draw if size is None: size = (600, 600) paper = Drawing("workflow_node.svg", size, id="repr") lg = paper.linearGradient((0.5, 0), (0.5, 1.), id="in_port") lg.add_stop_color(0, color='#3333ff') lg.add_stop_color(1, color='#2222ff') paper.defs.add(lg) lg = paper.linearGradient((0.5, 0), (0.5, 1.), id="out_port") lg.add_stop_color(0, color='#ffff33') lg.add_stop_color(1, color='#9a9a00') paper.defs.add(lg) # body g = paper.add(paper.g()) # background lg = paper.linearGradient((0.5, 0), (0.5, 1.)) lg.add_stop_color(0, color='#8c8cff') lg.add_stop_color(1, color='#c8c8c8') paper.defs.add(lg) bg = paper.rect((-nw / 2, -nh / 2), (nw, nh), rx=node_padding, ry=node_padding, stroke_width=1) bg.stroke('#808080') bg.fill(lg) g.add(bg) # label style = ('font-size: %dpx; font-family: %s; ' 'text-anchor: middle' % (label_font_size, label_font)) frag = paper.tspan(node['name'], dy=[label_font_size // 3]) label = paper.text("", style=style, fill='#000000') label.add(frag) g.add(label) # ports port_style = ('font-size: %dpx; ' % pfs + 'font-family: %s; ' % label_font) onstyle = port_style + 'text-anchor: end' instyle = port_style + 'text-anchor: start' istyle = port_style + 'text-anchor: middle' nb = len(node['inputs']) py = -nh / 2 for i, pdef in enumerate(node['inputs']): px = i * pspace - pspace * (nb - 1) / 2 pg = g.add(paper.g()) pg.translate(px, py) idef = store.get(pdef['interface'], None) if idef is not None and 'url' in idef: link = pg.add(paper.a(href=idef['url'], target='_top')) else: link = pg port = paper.circle((0, 0), pr, stroke='#000000', stroke_width=1) port.fill("url(#in_port)") link.add(port) # port name frag = paper.tspan(pdef['name'], dy=[-2 * pr]) label = paper.text("", style=instyle, fill='#000000') label.rotate(-45) label.add(frag) pg.add(label) # port interface if idef is None: itxt = pdef['interface'] else: itxt = idef['name'] if len(itxt) > 10: itxt = itxt[:7] + "..." frag = paper.tspan(itxt, dy=[pr + pfs]) label = paper.text("", style=istyle, fill='#000000') label.add(frag) link.add(label) nb = len(node['outputs']) py = nh / 2 for i, pdef in enumerate(node['outputs']): px = i * pspace - pspace * (nb - 1) / 2 pg = g.add(paper.g()) pg.translate(px, py) idef = store.get(pdef['interface'], None) if idef is not None and 'url' in idef: link = pg.add(paper.a(href=idef['url'], target='_top')) else: link = pg port = paper.circle((0, 0), pr, stroke='#000000', stroke_width=1) port.fill("url(#out_port)") link.add(port) # port name frag = paper.tspan(pdef['name'], dy=[2 * pr + pfs // 2]) label = paper.text("", style=onstyle, fill='#000000') label.rotate(-45) label.add(frag) pg.add(label) # port interface if idef is None: itxt = pdef['interface'] else: itxt = idef['name'] if len(itxt) > 10: itxt = itxt[:7] + "..." frag = paper.tspan(itxt, dy=[-pr - 2]) label = paper.text("", style=istyle, fill='#000000') label.add(frag) link.add(label) # reformat whole drawing to fit screen xmin = -nw / 2 - draw_padding / 10. xmax = +nw / 2 + draw_padding / 10. if len(node['inputs']) == 0: inames_extend = 0 else: inames = [(len(pdef['name']), pdef['name']) for pdef in node['inputs']] inames_extend = string_size(sorted(inames)[-1][1], pfs) * 0.7 + pfs ymin = -nh / 2 - pr - inames_extend - draw_padding / 10. if len(node['outputs']) == 0: onames_extend = 0 else: onames = [(len(pdef['name']), pdef['name']) for pdef in node['outputs']] onames_extend = string_size(sorted(onames)[-1][1], pfs) * 0.7 + pfs ymax = +nh / 2 + pr + onames_extend + draw_padding / 10. w = float(size[0]) h = float(size[1]) ratio = max((xmax - xmin) / w, (ymax - ymin) / h) xsize = ratio * w ysize = ratio * h bb = (xmin * xsize / (xmax - xmin), ymin * ysize / (ymax - ymin), xsize, ysize) paper.viewbox(*bb) return paper.tostring(), bb
def TSSOP(name, offset, width, height, padh, padw, spaceing, padcount, bodyw, bodyh, pinw, pinh, thermalw=0, thermalh=0): # document sizes doc = (width + 2 * offset[0], height + 2 * offset[1]) # basic SVG sheet svg = Drawing(filename=name, size=doc) ####################################################################################################### # PCB Pads ####################################################################################################### pads = svg.g(id="pads") for n in range(0, padcount): x = offset[0] + (n * spaceing) y = offset[1] pad = svg.rect(insert=(x, y), size=(padw, padh), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(pad) for n in range(0, padcount): x = offset[0] + (n * spaceing) y = offset[1] + height - padh pad = svg.rect(insert=(x, y), size=(padw, padh), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(pad) if thermalw is not 0 and thermalh is not 0: # thermal pad t = svg.rect(insert=((doc[0] / 2) - (thermalw / 2), (doc[1] / 2) - (thermalh / 2)), size=(thermalw, thermalh), stroke_width=0.05, stroke="black", fill="#D4AA00") pads.add(t) svg.add(pads) ####################################################################################################### # Part ####################################################################################################### cornerx = (doc[0] / 2) - (bodyw / 2) cornery = (doc[1] / 2) - (bodyh / 2) part = svg.g(id="part") for n in range(0, padcount): x = offset[0] + (n * spaceing) + ((padw - pinw) / 2) y = offset[1] + (padh - pinh) pin = svg.rect(insert=(x, y), size=(pinw, pinh), stroke_width=0.05, stroke="black", fill="#999999") part.add(pin) for n in range(0, padcount): x = offset[0] + (n * spaceing) + ((padw - pinw) / 2) y = offset[1] + (height - padh) pin = svg.rect(insert=(x, y), size=(pinw, pinh), stroke_width=0.05, stroke="black", fill="#999999") part.add(pin) body = svg.rect(insert=(cornerx, cornery), size=(bodyw, bodyh), stroke_width=0.05, stroke="black", fill="#333333") part.add(body) # pin 1 indicator ind = svg.circle(center=(cornerx + 0.5, cornery + bodyh - 0.5), r=0.2, stroke_width=0.05, stroke="black", fill="#333333") part.add(ind) svg.add(part) svg.save()
def disvg(paths=None, colors=None, filename=None, stroke_widths=None, nodes=None, node_colors=None, node_radii=None, openinbrowser=True, timestamp=None, margin_size=0.1, mindim=600, dimensions=None, viewbox=None, text=None, text_path=None, font_size=None, attributes=None, svg_attributes=None, svgwrite_debug=False, paths2Drawing=False, baseunit='px'): """Creates (and optionally displays) an SVG file. REQUIRED INPUTS: :param paths - a list of paths OPTIONAL INPUT: :param colors - specifies the path stroke color. By default all paths will be black (#000000). This paramater can be input in a few ways 1) a list of strings that will be input into the path elements stroke attribute (so anything that is understood by the svg viewer). 2) a string of single character colors -- e.g. setting colors='rrr' is equivalent to setting colors=['red', 'red', 'red'] (see the 'color_dict' dictionary above for a list of possibilities). 3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...]. :param filename - the desired location/filename of the SVG file created (by default the SVG will be named 'disvg_output.svg' or 'disvg_output_<timestamp>.svg' and stored in the temporary directory returned by `tempfile.gettempdir()`. See `timestamp` for information on the timestamp. :param stroke_widths - a list of stroke_widths to use for paths (default is 0.5% of the SVG's width or length) :param nodes - a list of points to draw as filled-in circles :param node_colors - a list of colors to use for the nodes (by default nodes will be red) :param node_radii - a list of radii to use for the nodes (by default nodes will be radius will be 1 percent of the svg's width/length) :param text - string or list of strings to be displayed :param text_path - if text is a list, then this should be a list of path (or path segments of the same length. Note: the path must be long enough to display the text or the text will be cropped by the svg viewer. :param font_size - a single float of list of floats. :param openinbrowser - Set to True to automatically open the created SVG in the user's default web browser. :param timestamp - if true, then the a timestamp will be appended to the output SVG's filename. This is meant as a workaround for issues related to rapidly opening multiple SVGs in your browser using `disvg`. This defaults to true if `filename is None` and false otherwise. :param margin_size - The min margin (empty area framing the collection of paths) size used for creating the canvas and background of the SVG. :param mindim - The minimum dimension (height or width) of the output SVG (default is 600). :param dimensions - The (x,y) display dimensions of the output SVG. I.e. this specifies the `width` and `height` SVG attributes. Note that these also can be used to specify units other than pixels. Using this will override the `mindim` parameter. :param viewbox - This specifies the coordinated system used in the svg. The SVG `viewBox` attribute works together with the the `height` and `width` attrinutes. Using these three attributes allows for shifting and scaling of the SVG canvas without changing the any values other than those in `viewBox`, `height`, and `width`. `viewbox` should be input as a 4-tuple, (min_x, min_y, width, height), or a string "min_x min_y width height". Using this will override the `mindim` parameter. :param attributes - a list of dictionaries of attributes for the input paths. Note: This will override any other conflicting settings. :param svg_attributes - a dictionary of attributes for output svg. :param svgwrite_debug - This parameter turns on/off `svgwrite`'s debugging mode. By default svgwrite_debug=False. This increases speed and also prevents `svgwrite` from raising of an error when not all `svg_attributes` key-value pairs are understood. :param paths2Drawing - If true, an `svgwrite.Drawing` object is returned and no file is written. This `Drawing` can later be saved using the `svgwrite.Drawing.save()` method. NOTES: * The `svg_attributes` parameter will override any other conflicting settings. * Any `extra` parameters that `svgwrite.Drawing()` accepts can be controlled by passing them in through `svg_attributes`. * The unit of length here is assumed to be pixels in all variables. * If this function is used multiple times in quick succession to display multiple SVGs (all using the default filename), the svgviewer/browser will likely fail to load some of the SVGs in time. To fix this, use the timestamp attribute, or give the files unique names, or use a pause command (e.g. time.sleep(1)) between uses. SEE ALSO: * document.py """ _default_relative_node_radius = 5e-3 _default_relative_stroke_width = 1e-3 _default_path_color = '#000000' # black _default_node_color = '#ff0000' # red _default_font_size = 12 if filename is None: timestamp = True if timestamp is None else timestamp filename = os_path.join(gettempdir(), 'disvg_output.svg') dirname = os_path.abspath(os_path.dirname(filename)) if not os_path.exists(dirname): makedirs(dirname) # append time stamp to filename if timestamp: fbname, fext = os_path.splitext(filename) tstamp = str(time()).replace('.', '') stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext filename = os_path.join(dirname, stfilename) # check paths and colors are set if isinstance(paths, Path) or is_path_segment(paths): paths = [paths] if paths: if not colors: colors = [_default_path_color] * len(paths) else: assert len(colors) == len(paths) if isinstance(colors, str): colors = str2colorlist(colors, default_color=_default_path_color) elif isinstance(colors, list): for idx, c in enumerate(colors): if is3tuple(c): colors[idx] = "rgb" + str(c) # check nodes and nodes_colors are set (node_radii are set later) if nodes: if not node_colors: node_colors = [_default_node_color] * len(nodes) else: assert len(node_colors) == len(nodes) if isinstance(node_colors, str): node_colors = str2colorlist(node_colors, default_color=_default_node_color) elif isinstance(node_colors, list): for idx, c in enumerate(node_colors): if is3tuple(c): node_colors[idx] = "rgb" + str(c) # set up the viewBox and display dimensions of the output SVG # along the way, set stroke_widths and node_radii if not provided assert paths or nodes stuff2bound = [] if viewbox: if not isinstance(viewbox, str): viewbox = '%s %s %s %s' % viewbox if dimensions is None: dimensions = viewbox.split(' ')[2:4] elif dimensions: dimensions = tuple(map(str, dimensions)) def strip_units(s): return re.search(r'\d*\.?\d*', s.strip()).group() viewbox = '0 0 %s %s' % tuple(map(strip_units, dimensions)) else: if paths: stuff2bound += paths if nodes: stuff2bound += nodes if text_path: stuff2bound += text_path xmin, xmax, ymin, ymax = big_bounding_box(stuff2bound) dx = xmax - xmin dy = ymax - ymin if dx == 0: dx = 1 if dy == 0: dy = 1 # determine stroke_widths to use (if not provided) and max_stroke_width if paths: if not stroke_widths: sw = max(dx, dy) * _default_relative_stroke_width stroke_widths = [sw] * len(paths) max_stroke_width = sw else: assert len(paths) == len(stroke_widths) max_stroke_width = max(stroke_widths) else: max_stroke_width = 0 # determine node_radii to use (if not provided) and max_node_diameter if nodes: if not node_radii: r = max(dx, dy) * _default_relative_node_radius node_radii = [r] * len(nodes) max_node_diameter = 2 * r else: assert len(nodes) == len(node_radii) max_node_diameter = 2 * max(node_radii) else: max_node_diameter = 0 extra_space_for_style = max(max_stroke_width, max_node_diameter) xmin -= margin_size * dx + extra_space_for_style / 2 ymin -= margin_size * dy + extra_space_for_style / 2 dx += 2 * margin_size * dx + extra_space_for_style dy += 2 * margin_size * dy + extra_space_for_style viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy) if mindim is None: szx = "{}{}".format(dx, baseunit) szy = "{}{}".format(dy, baseunit) else: if dx > dy: szx = str(mindim) + baseunit szy = str(int(ceil(mindim * dy / dx))) + baseunit else: szx = str(int(ceil(mindim * dx / dy))) + baseunit szy = str(mindim) + baseunit dimensions = szx, szy # Create an SVG file if svg_attributes is not None: dimensions = (svg_attributes.get("width", dimensions[0]), svg_attributes.get("height", dimensions[1])) debug = svg_attributes.get("debug", svgwrite_debug) dwg = Drawing(filename=filename, size=dimensions, debug=debug, **svg_attributes) else: dwg = Drawing(filename=filename, size=dimensions, debug=svgwrite_debug, viewBox=viewbox) # add paths if paths: for i, p in enumerate(paths): if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p if attributes: good_attribs = {'d': ps} for key in attributes[i]: val = attributes[i][key] if key != 'd': try: dwg.path(ps, **{key: val}) good_attribs.update({key: val}) except Exception as e: warn(str(e)) dwg.add(dwg.path(**good_attribs)) else: dwg.add( dwg.path(ps, stroke=colors[i], stroke_width=str(stroke_widths[i]), fill='none')) # add nodes (filled in circles) if nodes: for i_pt, pt in enumerate([(z.real, z.imag) for z in nodes]): dwg.add(dwg.circle(pt, node_radii[i_pt], fill=node_colors[i_pt])) # add texts if text: assert isinstance(text, str) or (isinstance(text, list) and isinstance( text_path, list) and len(text_path) == len(text)) if isinstance(text, str): text = [text] if not font_size: font_size = [_default_font_size] if not text_path: pos = complex(xmin + margin_size * dx, ymin + margin_size * dy) text_path = [Line(pos, pos + 1).d()] else: if font_size: if isinstance(font_size, list): assert len(font_size) == len(text) else: font_size = [font_size] * len(text) else: font_size = [_default_font_size] * len(text) for idx, s in enumerate(text): p = text_path[idx] if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p # paragraph = dwg.add(dwg.g(font_size=font_size[idx])) # paragraph.add(dwg.textPath(ps, s)) pathid = 'tp' + str(idx) dwg.defs.add(dwg.path(d=ps, id=pathid)) txter = dwg.add(dwg.text('', font_size=font_size[idx])) txter.add(txt.TextPath('#' + pathid, s)) if paths2Drawing: return dwg dwg.save() # re-open the svg, make the xml pretty, and save it again xmlstring = md_xml_parse(filename).toprettyxml() with open(filename, 'w') as f: f.write(xmlstring) # try to open in web browser if openinbrowser: try: open_in_browser(filename) except: print("Failed to open output SVG in browser. SVG saved to:") print(filename)
def _create_group(self, drawing: Drawing, projection: np.ndarray, viewport: Viewport, group: Group): """ Render all the meshes contained in this group. The main consideration here is that we will consider the z-index of every object in this group. """ default_style = group.style or {} shaders = [ mesh.shader or (lambda face_index, winding: {}) for mesh in group.meshes ] annotators = [ mesh.annotator or (lambda face_index: None) for mesh in group.meshes ] # A combination of mesh and face indes mesh_faces: List[np.ndarray] = [] for i, mesh in enumerate(group.meshes): faces = mesh.faces # Extend each point to a vec4, then transform to clip space. faces = np.dstack([faces, np.ones(faces.shape[:2])]) faces = np.dot(faces, projection) # Reject trivially clipped polygons. xyz, w = faces[:, :, :3], faces[:, :, 3:] accepted = np.logical_and(np.greater(xyz, -w), np.less(xyz, +w)) accepted = np.all(accepted, 2) # vert is accepted if xyz are all inside accepted = np.any(accepted, 1) # face is accepted if any vert is inside degenerate = np.less_equal(w, 0)[:, :, 0] # vert is bad if its w <= 0 degenerate = np.any(degenerate, 1) # face is bad if any of its verts are bad accepted = np.logical_and(accepted, np.logical_not(degenerate)) faces = np.compress(accepted, faces, axis=0) # Apply perspective transformation. xyz, w = faces[:, :, :3], faces[:, :, 3:] faces = xyz / w mesh_faces.append(faces) # Sort faces from back to front. mesh_face_indices = self._sort_back_to_front(mesh_faces) # Apply viewport transform to X and Y. for faces in mesh_faces: faces[:, :, 0:1] = (1.0 + faces[:, :, 0:1]) * viewport.width / 2 faces[:, :, 1:2] = (1.0 - faces[:, :, 1:2]) * viewport.height / 2 faces[:, :, 0:1] += viewport.minx faces[:, :, 1:2] += viewport.miny # Compute the winding direction of each polygon. mesh_windings: List[np.ndarray] = [] for faces in mesh_faces: windings = np.zeros(faces.shape[0]) if faces.shape[1] >= 3: p0, p1, p2 = faces[:, 0, :], faces[:, 1, :], faces[:, 2, :] normals = np.cross(p2 - p0, p1 - p0) np.copyto(windings, normals[:, 2]) mesh_windings.append(windings) group_ = drawing.g(**default_style) text_group_ = drawing.g(**default_style) # Finally draw the group for mesh_index, face_index in mesh_face_indices: face = mesh_faces[mesh_index][face_index] style = shaders[mesh_index](face_index, mesh_windings[mesh_index][face_index]) if style is None: continue face = np.around(face[:, :2], self.precision) if len(face) == 1: group_.add( drawing.circle(face[0], style.pop("radius", 0.005), **style)) if len(face) == 2: group_.add(drawing.line(face[0], face[1], **style)) else: group_.add(drawing.polygon(face, **style)) annotation = annotators[mesh_index](face_index) if annotation is not None: centroid = face.mean(axis=0) text_group_.add(drawing.text(insert=centroid, **annotation)) return [group_, text_group_]