""" To be used in Python 3.6+ due to dependencies """ from copy import copy import random import string import os import numpy as np from scipy.spatial.transform import Rotation as R # Correct dependencies on Win # https://stackoverflow.com/questions/46265677/get-cairosvg-working-in-windows if 'Windows' in os.environ.get('OS',''): dir_path = os.path.dirname(os.path.realpath(__file__)) os.environ['path'] += f';{os.path.abspath(dir_path + "/cairo_dlls/")}' import cairosvg import svgpathtools as svgpath import svgwrite as sw import matplotlib.pyplot as plt # my from pygarment import data_config from . import core from .utils import * class VisPattern(core.ParametrizedPattern): """ "Visualizible" pattern wrapper of pattern specification in custom JSON format. Input: * Pattern template in custom JSON format Output representations: * Pattern instance in custom JSON format * In the current state * SVG (stitching info is lost) * PNG for visualization Not implemented: * Support for patterns with darts NOTE: Visualization assumes the pattern uses cm as units """ # ------------ Interface ------------- def __init__(self, pattern_file=None): super().__init__(pattern_file) self.px_per_unit = 3 def serialize( self, path, to_subfolder=True, tag='', with_3d=True, with_text=True, view_ids=True, with_printable=False, empty_ok=False): log_dir = super().serialize(path, to_subfolder, tag=tag, empty_ok=empty_ok) if len(self.panel_order()) == 0: # If we are still here, but pattern is empty, don't generate an image return log_dir if tag: tag = '_' + tag svg_file = os.path.join(log_dir, (self.name + tag + '_pattern.svg')) svg_printable_file = os.path.join(log_dir, (self.name + tag + '_print_pattern.svg')) png_file = os.path.join(log_dir, (self.name + tag + '_pattern.png')) pdf_file = os.path.join(log_dir, (self.name + tag + '_print_pattern.pdf')) png_3d_file = os.path.join(log_dir, (self.name + tag + '_3d_pattern.png')) # save visualtisation self._save_as_image(svg_file, png_file, with_text, view_ids) if with_3d: self._save_as_image_3D(png_3d_file) if with_printable: self._save_as_pdf(svg_printable_file, pdf_file, with_text, view_ids) return log_dir # -------- Drawing --------- def _verts_to_px_coords(self, vertices, translation_2d): """Convert given vertices and panel (2D) translation to px coordinate frame & units""" # Flip Y coordinate (in SVG Y looks down) vertices[:, 1] *= -1 translation_2d[1] *= -1 # Put upper left corner of the bounding box at zero offset = np.min(vertices, axis=0) vertices = vertices - offset translation_2d = translation_2d + offset return vertices, translation_2d def _flip_y(self, point): """ To get to image coordinates one might need to flip Y axis """ flipped_point = list(point) # top-level copy flipped_point[1] *= -1 return flipped_point def _draw_a_panel(self, panel_name, apply_transform=True, fill=True): """ Adds a requested panel to the svg drawing with given offset and scaling Assumes (!!) that edges are correctly oriented to form a closed loop Returns the lower-right vertex coordinate for the convenice of future offsetting. """ attributes = { 'fill': 'rgb(115, 113, 125)' if fill else 'rgb(255,255,255)', # fill with white 'stroke': 'rgb(51,51,51)', 'stroke-width': '0.2' } panel = self.pattern['panels'][panel_name] vertices = np.asarray(panel['vertices']) vertices, translation = self._verts_to_px_coords( vertices, np.array(panel['translation'][:2])) # Only XY # draw edges segs = [self._edge_as_curve(vertices, edge) for edge in panel['edges']] path = svgpath.Path(*segs) if apply_transform: # Placement and rotation according to the 3D location # But flatterened on 2D # Z-fist rotation to only reflect rotation visible in XY plane # NOTE: Heuristic, might be bug-prone rotation = R.from_euler('XYZ', panel['rotation'], degrees=True) # XYZ # Estimate degree of rotation of Y axis # NOTE: Ox sometimes gets flipped because of # Gimbal locks of this Euler angle representation res = rotation.apply([0, 1, 0]) flat_rot_angle = np.rad2deg(vector_angle([0, 1], res[:2])) path = path.rotated( degs=-flat_rot_angle, origin=list_to_c(vertices[0]) ) path = path.translated(list_to_c(translation)) # NOTE: rot/transl order is important! return path, attributes, panel['translation'][-1] >= 0 def _add_panel_annotations( self, drawing, panel_name, path:svgpath.Path, with_text=True, view_ids=True): """ Adds a annotations for requested panel to the svg drawing with given offset and scaling Assumes (!!) that edges are correctly oriented to form a closed loop Returns the lower-right vertex coordinate for the convenice of future offsetting. """ bbox = path.bbox() panel_center = np.array([(bbox[0] + bbox[1]) / 2, (bbox[2] + bbox[3]) / 2]) if with_text: text_insert = panel_center # + np.array([-len(panel_name) * 12 / 2, 3]) drawing.add(drawing.text(panel_name, insert=text_insert, fill='rgb(31,31,31)', font_size='7', text_anchor='middle', dominant_baseline='middle')) if view_ids: # name vertices for idx in range(len(path)): seg = path[idx] ver = c_to_np(seg.start) drawing.add( drawing.text(str(idx), insert=ver, fill='rgb(245,96,66)', font_size='7')) # name edges for idx in range(len(path)): seg = path[idx] middle = c_to_np(seg.point(seg.ilength(seg.length() / 2, s_tol=1e-3))) middle[1] -= 3 # slightly above the line # name drawing.add( drawing.text(idx, insert=middle, fill='rgb(44,131,68)', font_size='7', text_anchor='middle')) def get_svg(self, svg_filename, with_text=True, view_ids=True, flat=False, fill_panels=True, margin=2) -> sw.Drawing: """Convert pattern to writable svg representation""" if len(self.panel_order()) == 0: # If we are still here, but pattern is empty, don't generate an image raise core.EmptyPatternError() # Get svg representation per panel # Order by depth (=> most front panels render in front) # TODOLOW Even smarter way is needed for prettier allignment panel_order = self.panel_order() panel_z = [self.pattern['panels'][pn]['translation'][-1] for pn in panel_order] z_sorted_panels = [p for _, p in sorted(zip(panel_z, panel_order))] # Get panel paths paths_front, paths_back = [], [] attributes_f, attributes_b = [], [] names_f, names_b = [], [] shift_x_front, shift_x_back = margin, margin for panel in z_sorted_panels: if panel is not None: path, attr, front = self._draw_a_panel( panel, apply_transform=not flat, fill=fill_panels ) if flat: path = path.translated(list_to_c([ shift_x_front if front else shift_x_back, 0])) bbox = path.bbox() diff = (bbox[1] - bbox[0]) + margin if front: shift_x_front += diff else: shift_x_back += diff if front: paths_front.append(path) attributes_f.append(attr) names_f.append(panel) else: paths_back.append(path) attributes_b.append(attr) names_b.append(panel) # Shift back panels if both front and back exist if len(paths_front) > 0 and len(paths_back) > 0: front_max_x = max([path.bbox()[1] for path in paths_front]) back_min_x = min([path.bbox()[0] for path in paths_back]) shift_x = front_max_x - back_min_x + 10 # A little spacing if flat: front_max_y = max([path.bbox()[3] for path in paths_front]) back_min_y = min([path.bbox()[2] for path in paths_back]) shift_y = front_max_y - back_min_y + 10 # A little spacing shift_x = 0 else: shift_y = 0 paths_back = [path.translated(list_to_c([shift_x, shift_y])) for path in paths_back] # SVG convert paths = paths_front + paths_back arrdims = np.array([path.bbox() for path in paths]) dims = np.max(arrdims[:, 1]) - np.min(arrdims[:, 0]), np.max(arrdims[:, 3]) - np.min(arrdims[:, 2]) viewbox = ( np.min(arrdims[:, 0]) - margin, np.min(arrdims[:, 2]) - margin, dims[0] + 2 * margin, dims[1] + 2 * margin ) # Pattern info for correct placement self.svg_bbox = [np.min(arrdims[:, 0]), np.max(arrdims[:, 1]), np.min(arrdims[:, 2]), np.max(arrdims[:, 3])] self.svg_bbox_size = [viewbox[2], viewbox[3]] # Save attributes = attributes_f + attributes_b dwg = svgpath.wsvg( paths, attributes=attributes, margin_size=0, filename=svg_filename, viewbox=viewbox, dimensions=[str(viewbox[2]) + 'cm', str(viewbox[3]) + 'cm'], paths2Drawing=True) # text annotations panel_names = names_f + names_b if with_text or view_ids: for i, panel in enumerate(panel_names): if panel is not None: self._add_panel_annotations( dwg, panel, paths[i], with_text, view_ids) return dwg def _save_as_image( self, svg_filename, png_filename, with_text=True, view_ids=True, margin=2): """ Saves current pattern in svg and png format for visualization * with_text: include panel names * view_ids: include ids of vertices and edges in the output image * margin: small amount of free space around the svg drawing (to correctly display the line width) """ dwg = self.get_svg( svg_filename, with_text=with_text, view_ids=view_ids, flat=False, margin=margin ) dwg.save(pretty=True) # to png # NOTE: Assuming the pattern uses cm # 3 px == 1 cm # DPI = 96 (default) px/inch == 96/2.54 px/cm cairosvg.svg2png( url=svg_filename, write_to=png_filename, dpi=2.54*self.px_per_unit) def _save_as_image_3D(self, png_filename): """Save the patterns with 3D positioning using matplotlib visualization""" # NOTE: this routine is mostly needed for debugging fig = plt.figure(figsize=(30 / 2.54, 30 / 2.54)) ax = fig.add_subplot(projection='3d') # TODOLOW Support arcs / curves (use linearization) for panel in self.pattern['panels']: p = self.pattern['panels'][panel] rot = p['rotation'] tr = p['translation'] verts_2d = p['vertices'] verts_to_plot = copy(verts_2d) verts_to_plot.append(verts_to_plot[0]) verts3d = np.vstack(tuple([self._point_in_3D(v, rot, tr) for v in verts_to_plot])) x = np.squeeze(np.asarray(verts3d[:, 0])) y = np.squeeze(np.asarray(verts3d[:, 1])) z = np.squeeze(np.asarray(verts3d[:, 2])) ax.plot(x, y, z) ax.view_init(elev=115, azim=-59, roll=30) ax.set_aspect('equal') fig.savefig(png_filename, dpi=300, transparent=False) plt.close(fig) # Cleanup def _save_as_pdf(self, svg_filename, pdf_filename, with_text=True, view_ids=True, margin=2): """Save a pattern as a pdf with non-overlapping panels and no filling Suitable for printing """ dwg = self.get_svg( svg_filename, with_text=with_text, view_ids=view_ids, flat=True, fill_panels=False, margin=margin ) dwg.save(pretty=True) # to pdf # NOTE: Assuming the pattern uses cm # 3 px == 1 cm # DPI = 96 (default) px/inch == 96/2.54 px/cm cairosvg.svg2pdf( url=svg_filename, write_to=pdf_filename, dpi=2.54*self.px_per_unit) class RandomPattern(VisPattern): """ Parameter randomization of a pattern template in custom JSON format. Input: * Pattern template in custom JSON format Output representations: * Pattern instance in custom JSON format (with updated parameter values and vertex positions) * SVG (stitching info is lost) * PNG for visualization Implementation limitations: * Parameter randomization is only performed once on loading * Only accepts unchanged template files (all parameter values = 1) otherwise, parameter values will go out of control and outside of the original range (with no way to recognise it) """ # ------------ Interface ------------- def __init__(self, template_file): """Note that this class requires some input file: there is not point of creating this object with empty pattern""" super().__init__(template_file, view_ids=False) # don't show ids for datasets # update name for a random pattern self.name = self.name + '_' + self._id_generator() # randomization setup self._randomize_pattern() # -------- Other Utils --------- def _id_generator(self, size=10, chars=string.ascii_uppercase + string.digits): """Generated a random string of a given size, see https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits """ return ''.join(random.choices(chars, k=size))