def efill(document: vp.Document, tolerance: float, distance: float): """ Implements the Eulerian fill algorithm which fills any closed shapes with as few paths as there are contiguous regions. With scanlines to fill any shapes, even those with holes, with an even-odd fill order and direct pathing. """ for layer in list(document.layers.values() ): # Add all the closed paths to the efill. efill = EulerianFill(distance) for p in layer: if np.abs(p[0] - p[-1]) <= tolerance: efill += vp.as_vector(p) fill = efill.get_fill() # Get the resulting fill. lc = vp.LineCollection() cur_line = [] for pt in fill: if pt is None: if cur_line: lc.append(cur_line) cur_line = [] else: cur_line.append(complex(pt[0], pt[1])) if cur_line: lc.append(cur_line) document.add(lc) return document
def stylize_path(line: np.ndarray, weight: int, pen_width: float, detail: float) -> vp.LineCollection: """Implement a heavy stroke weight by buffering multiple times the base path. Note: recursive buffering is to be avoided to properly control detail! """ if weight == 1: return vp.LineCollection([line]) lc = vp.LineCollection() # path to be used as starting point for buffering geom = LineString(vp.as_vector(line)) if weight % 2 == 0: radius = pen_width / 2 _add_to_line_collection( geom.buffer(radius, resolution=_calc_buffer_resolution(radius, detail)), lc) else: radius = 0.0 _add_to_line_collection(geom, lc) for i in range((weight - 1) // 2): radius += pen_width p = geom.buffer(radius, resolution=_calc_buffer_resolution(radius, detail)) _add_to_line_collection(p, lc) return lc
def _build_buffers(lc: vp.LineCollection) -> Tuple[np.ndarray, np.ndarray]: # build index array ranges: List[Sequence[int]] = [] block = [] cur_index = 0 restart_mark = [-1] for line in lc: ranges.append(range(cur_index, cur_index + len(line))) ranges.append(restart_mark) cur_index += len(line) block.append(vp.as_vector(line)) return np.vstack(block), np.concatenate(ranges)
def _build_buffers( lc: vp.LineCollection, color: ColorType = (0.0, 0.0, 0.0, 1.0) ) -> Tuple[np.ndarray, np.ndarray]: # build index array ranges: List[Sequence[int]] = [] block = [] cur_index = 0 restart_mark = [-1] for line in lc: ranges.append(range(cur_index, cur_index + len(line))) ranges.append(restart_mark) cur_index += len(line) block.append([vp.as_vector(line), np.tile(color, (len(line), 1))]) return np.block(block), np.concatenate(ranges)
def _build_buffers(cls, lc: vp.LineCollection) -> Tuple[np.ndarray, np.ndarray]: # build index array ranges: List[Sequence[int]] = [] block = [] cur_index = 0 restart_mark = [-1] for i, line in enumerate(lc): n = len(line) ranges.append(range(cur_index, cur_index + n)) ranges.append(restart_mark) cur_index += n color = cls.COLORS[i % len(cls.COLORS)] colors = np.tile(color, (n, 1)) colors[::2, 0:3] *= 0.6 block.append([vp.as_vector(line), colors]) return np.block(block), np.concatenate(ranges)
def dbsample(vector_data: VectorData): """ Show statistics on the current geometries in JSON format. """ global debug_data data: Dict[str, Any] = {} if vector_data.is_empty(): data["count"] = 0 else: data["count"] = sum(len(lc) for lc in vector_data.layers.values()) data["layer_count"] = len(vector_data.layers) data["length"] = vector_data.length() data["pen_up_length"] = vector_data.pen_up_length() data["bounds"] = vector_data.bounds() data["layers"] = { layer_id: [as_vector(line).tolist() for line in layer] for layer_id, layer in vector_data.layers.items() } debug_data.append(data) return vector_data
def _build_buffers(lc: vp.LineCollection): """Prepare the buffers for multi-polyline rendering. Closed polyline must have their last point identical to their first point.""" indices = [] reset_index = [-1] start_index = 0 for i, line in enumerate(lc): if line[0] == line[-1]: # closed path idx = np.arange(len(line) + 3) - 1 idx[0], idx[-2], idx[-1] = len(line) - 1, 0, 1 else: idx = np.arange(len(line) + 2) - 1 idx[0], idx[-1] = 0, len(line) - 1 indices.append(idx + start_index) start_index += len(line) indices.append(reset_index) return ( np.vstack([vp.as_vector(line).astype("f4") for line in lc]), np.concatenate(indices).astype("i4"), )
def show( vector_data: VectorData, show_axes: bool, show_grid: bool, show_pen_up: bool, show_points: bool, hide_legend: bool, colorful: bool, unit: str, ): """ Display the geometry using matplotlib. By default, only the geometries are displayed without the axis. All geometries are displayed with black. When using the `--colorful` flag, each segment will have a different color (default matplotlib behaviour). This can be useful for debugging purposes. """ scale = 1 / convert(unit) fig = plt.figure() color_idx = 0 collections = {} for layer_id, lc in vector_data.layers.items(): if colorful: color = COLORS[color_idx:] + COLORS[:color_idx] marker_color = "k" color_idx += len(lc) else: color = COLORS[color_idx] # type: ignore marker_color = [color] # type: ignore color_idx += 1 if color_idx >= len(COLORS): color_idx = color_idx % len(COLORS) layer_lines = matplotlib.collections.LineCollection( (as_vector(line) * scale for line in lc), color=color, lw=1, alpha=0.5, label=str(layer_id), ) collections[layer_id] = [layer_lines] plt.gca().add_collection(layer_lines) if show_points: points = np.hstack([line for line in lc]) * scale layer_points = plt.gca().scatter(points.real, points.imag, marker=".", c=marker_color, s=16) collections[layer_id].append(layer_points) if show_pen_up: pen_up_lines = matplotlib.collections.LineCollection( ((as_vector(lc[i])[-1] * scale, as_vector(lc[i + 1])[0] * scale) for i in range(len(lc) - 1)), color=(0, 0, 0), lw=0.5, alpha=0.5, ) collections[layer_id].append(pen_up_lines) plt.gca().add_collection(pen_up_lines) plt.gca().invert_yaxis() plt.axis("equal") plt.margins(0, 0) if not hide_legend: lgd = plt.legend() # we will set up a dict mapping legend line to orig line, and enable # picking on the legend line line_dict = {} for lgd_line, lgd_text in zip(lgd.get_lines(), lgd.get_texts()): lgd_line.set_picker(5) # 5 pts tolerance layer_id = int(lgd_text.get_text()) if layer_id in collections: line_dict[lgd_line] = collections[layer_id] def on_pick(event): line = event.artist vis = not line_dict[line][0].get_visible() for ln in line_dict[line]: ln.set_visible(vis) if vis: line.set_alpha(1.0) else: line.set_alpha(0.2) fig.canvas.draw() fig.canvas.mpl_connect("pick_event", on_pick) if show_axes or show_grid: plt.axis("on") plt.xlabel(f"[{unit}]") plt.ylabel(f"[{unit}]") else: plt.axis("off") if show_grid: plt.grid("on") plt.show() return vector_data
def display_matplotlib( vector_data: Union[vp.LineCollection, vp.VectorData], page_format: Tuple[float, float] = None, center: bool = False, show_axes: bool = True, show_grid: bool = False, show_pen_up: bool = False, colorful: bool = False, unit: str = "px", fig_size: Tuple[float, float] = None, ) -> None: if isinstance(vector_data, vp.LineCollection): vector_data = vp.VectorData(vector_data) scale = 1 / vp.convert(unit) if fig_size: plt.figure(figsize=fig_size) plt.cla() # draw page if page_format is not None: w = page_format[0] * scale h = page_format[1] * scale dw = 10 * scale plt.fill( np.array([w, w + dw, w + dw, dw, dw, w]), np.array([dw, dw, h + dw, h + dw, h, h]), "k", alpha=0.3, ) plt.plot( np.array([0, 1, 1, 0, 0]) * w, np.array([0, 0, 1, 1, 0]) * h, "-k", lw=0.25, ) # compute offset offset = complex(0, 0) if center and page_format: bounds = vector_data.bounds() if bounds is not None: offset = complex( (page_format[0] - (bounds[2] - bounds[0])) / 2.0 - bounds[0], (page_format[1] - (bounds[3] - bounds[1])) / 2.0 - bounds[1], ) offset_ndarr = np.array([offset.real, offset.imag]) # plot all layers color_idx = 0 collections = {} for layer_id, lc in vector_data.layers.items(): if colorful: color: Union[Tuple[float, float, float], List[Tuple[ float, float, float]]] = COLORS[color_idx:] + COLORS[:color_idx] color_idx += len(lc) else: color = COLORS[color_idx] color_idx += 1 if color_idx >= len(COLORS): color_idx = color_idx % len(COLORS) # noinspection PyUnresolvedReferences layer_lines = matplotlib.collections.LineCollection( (vp.as_vector(line + offset) * scale for line in lc), color=color, lw=1, alpha=0.5, label=str(layer_id), ) collections[layer_id] = [layer_lines] plt.gca().add_collection(layer_lines) if show_pen_up: # noinspection PyUnresolvedReferences pen_up_lines = matplotlib.collections.LineCollection( (( (vp.as_vector(lc[i])[-1] + offset_ndarr) * scale, (vp.as_vector(lc[i + 1])[0] + offset_ndarr) * scale, ) for i in range(len(lc) - 1)), color=(0, 0, 0), lw=0.5, alpha=0.5, ) collections[layer_id].append(pen_up_lines) plt.gca().add_collection(pen_up_lines) plt.gca().invert_yaxis() plt.axis("equal") plt.margins(0, 0) if show_axes or show_grid: plt.axis("on") plt.xlabel(f"[{unit}]") plt.ylabel(f"[{unit}]") else: plt.axis("off") if show_grid: plt.grid("on") plt.show()
def write( vector_data: VectorData, output, single_path: bool, page_format: str, landscape: bool, center: bool, ): """ Save geometries to a SVG file. By default, the SVG generated has bounds tightly fit around the geometries. Optionally, a page format can be provided (`--page-format`). In this case, the geometries are not scaled or translated by default, even if they lie outside of the page bounds. The `--center` option translates the geometries to the center of the page. If output path is `-`, SVG content is output on stdout. """ if vector_data.is_empty(): logging.warning("no geometry to save, no file created") return vector_data # compute bounds bounds = vector_data.bounds() if page_format != "tight": size = tuple(c * 96.0 / 25.4 for c in PAGE_FORMATS[page_format]) if landscape: size = tuple(reversed(size)) else: size = (bounds[2] - bounds[0], bounds[3] - bounds[1]) if center: corrected_vector_data = copy.deepcopy(vector_data) corrected_vector_data.translate( (size[0] - (bounds[2] - bounds[0])) / 2.0 - bounds[0], (size[1] - (bounds[3] - bounds[1])) / 2.0 - bounds[1], ) elif page_format == "tight": corrected_vector_data = copy.deepcopy(vector_data) corrected_vector_data.translate(-bounds[0], -bounds[1]) else: corrected_vector_data = vector_data # output SVG dwg = svgwrite.Drawing(size=size, profile="tiny", debug=False) dwg.attribs["xmlns:inkscape"] = "http://www.inkscape.org/namespaces/inkscape" for layer_id in sorted(corrected_vector_data.layers.keys()): layer = corrected_vector_data.layers[layer_id] group = dwg.g( style="display:inline", id=f"layer{layer_id}", fill="none", stroke="black" ) group.attribs["inkscape:groupmode"] = "layer" group.attribs["inkscape:label"] = str(layer_id) if single_path: group.add( dwg.path( " ".join( ("M" + " L".join(f"{x},{y}" for x, y in as_vector(line))) for line in layer ), ) ) else: for line in layer: group.add(dwg.path("M" + " L".join(f"{x},{y}" for x, y in as_vector(line)),)) dwg.add(group) dwg.write(output, pretty=True) return vector_data
def _show_mpl( document: vp.Document, show_axes: bool, show_grid: bool, show_pen_up: bool, show_points: bool, hide_legend: bool, colorful: bool, unit: str, ): """Display the geometry using matplotlib. By default, only the geometries are displayed without the axis. All geometries are displayed with black. When using the `--colorful` flag, each segment will have a different color (default matplotlib behaviour). This can be useful for debugging purposes. """ # deferred import to optimise startup time import matplotlib.collections import matplotlib.pyplot as plt scale = 1 / vp.convert_length(unit) fig = plt.figure() color_idx = 0 collections = {} # draw page boundaries if document.page_size: w = document.page_size[0] * scale h = document.page_size[1] * scale dw = 10 * scale plt.plot( np.array([0, 1, 1, 0, 0]) * w, np.array([0, 0, 1, 1, 0]) * h, "-k", lw=0.25, label=None, ) plt.fill( np.array([w, w + dw, w + dw, dw, dw, w]), np.array([dw, dw, h + dw, h + dw, h, h]), "k", alpha=0.3, label=None, ) for layer_id, lc in document.layers.items(): if colorful: color = COLORS[color_idx:] + COLORS[:color_idx] marker_color = "k" color_idx += len(lc) else: color = COLORS[color_idx] # type: ignore marker_color = [color] # type: ignore color_idx += 1 if color_idx >= len(COLORS): color_idx = color_idx % len(COLORS) layer_lines = matplotlib.collections.LineCollection( (vp.as_vector(line) * scale for line in lc), color=color, lw=1, alpha=0.5, label=str(layer_id), ) collections[layer_id] = [layer_lines] plt.gca().add_collection(layer_lines) if show_points: points = np.hstack([line for line in lc]) * scale layer_points = plt.gca().scatter(points.real, points.imag, marker=".", c=marker_color, s=16) collections[layer_id].append(layer_points) if show_pen_up: pen_up_lines = matplotlib.collections.LineCollection( ((vp.as_vector(lc[i])[-1] * scale, vp.as_vector(lc[i + 1])[0] * scale) for i in range(len(lc) - 1)), color=(0, 0, 0), lw=0.5, alpha=0.5, ) collections[layer_id].append(pen_up_lines) plt.gca().add_collection(pen_up_lines) plt.gca().invert_yaxis() plt.axis("equal") plt.margins(0, 0) if not hide_legend: lgd = plt.legend(loc="upper right") # we will set up a dict mapping legend line to orig line, and enable # picking on the legend line line_dict = {} for lgd_line, lgd_text in zip(lgd.get_lines(), lgd.get_texts()): lgd_line.set_picker(True) # 5 pts tolerance lgd_line.set_pickradius(5) layer_id = int(lgd_text.get_text()) if layer_id in collections: line_dict[lgd_line] = collections[layer_id] def on_pick(event): line = event.artist vis = not line_dict[line][0].get_visible() for ln in line_dict[line]: ln.set_visible(vis) if vis: line.set_alpha(1.0) else: line.set_alpha(0.2) fig.canvas.draw() fig.canvas.mpl_connect("pick_event", on_pick) if show_axes or show_grid: plt.axis("on") plt.xlabel(f"[{unit}]") plt.ylabel(f"[{unit}]") else: plt.axis("off") if show_grid: plt.grid("on") plt.show()
def _replot(self): if not self._init_lims: old_lims = (self.ax.get_xlim(), self.ax.get_ylim()) else: old_lims = None self._init_lims = False self.ax.cla() scale = 1 / vpype.convert(self._unit) # draw page w = self._page_format[0] * scale h = self._page_format[1] * scale dw = 10 * scale self.ax.plot( np.array([0, 1, 1, 0, 0]) * w, np.array([0, 0, 1, 1, 0]) * h, "-k", lw=0.25, ) self.ax.fill( np.array([w, w + dw, w + dw, dw, dw, w]), np.array([dw, dw, h + dw, h + dw, h, h]), "k", alpha=0.3, ) color_idx = 0 for layer_id, lc in self._vector_data.layers.items(): if self._colorful: color = COLORS[color_idx:] + COLORS[:color_idx] marker_color = "k" color_idx += len(lc) if color_idx >= len(COLORS): color_idx = color_idx % len(COLORS) else: color = self._layers[layer_id].color marker_color = [color] layer_lines = matplotlib.collections.LineCollection( (vpype.as_vector(line) * scale for line in lc), color=color, lw=1, alpha=0.5, label=str(layer_id), ) self._layers[layer_id].lines = [layer_lines] self.ax.add_collection(layer_lines) if self._show_points: points = np.hstack([line for line in lc]) * scale layer_points = self.ax.scatter(points.real, points.imag, marker=".", c=marker_color, s=16) self._layers[layer_id].lines.append(layer_points) if self._show_pen_up: pen_up_lines = matplotlib.collections.LineCollection( (( vpype.as_vector(lc[i])[-1] * scale, vpype.as_vector(lc[i + 1])[0] * scale, ) for i in range(len(lc) - 1)), color=(0, 0, 0), lw=0.5, alpha=0.5, ) self._layers[layer_id].lines.append(pen_up_lines) self.ax.add_collection(pen_up_lines) self.ax.invert_yaxis() self.ax.axis("equal") # set visibility for layer_spec in self._layers.values(): if not layer_spec.visible: for ln in layer_spec.lines: ln.set_visible(False) if self._show_axes or self._show_grid: self.ax.axis("on") self.ax.set_xlabel(f"[{self._unit}]") self.ax.set_ylabel(f"[{self._unit}]") else: self.ax.axis("off") if self._show_grid: self.ax.grid("on", alpha=0.2) if old_lims is not None: self.ax.set_xlim(old_lims[0]) self.ax.set_ylim(old_lims[1]) else: self.toolbar.update() for text in self.ax.get_xticklabels(): text.set_horizontalalignment("center") text.set_verticalalignment("bottom") for text in self.ax.get_yticklabels(): text.set_horizontalalignment("left") text.set_verticalalignment("center") self.canvas.draw()
def circlecrop(lines: vp.LineCollection, x: float, y: float, r: float, quantization: float): """Crop to a circular area.""" circle = Polygon(vp.as_vector(vp.circle(x, y, r, quantization))) mls = lines.as_mls() return vp.LineCollection(mls.intersection(circle))