def vpype_text(string, font, size, position, align): """ Generate text using a Hershey font. """ # skip if text is empty if string.strip() == "": return vp.LineCollection() lines = axi.text(string, font=FONTS[font]) lc = vp.LineCollection() for line in lines: lc.append([x + 1j * y for x, y in line]) # by default, axi's font appear to be approx 18px size scale_factor = size / 18.0 lc.scale(scale_factor, scale_factor) min_x, _, max_x, _ = lc.bounds() if align == "left": lc.translate(-min_x, 0) elif align == "center": lc.translate(-(max_x - min_x) / 2, 0) elif align == "right": lc.translate(-max_x, 0) else: logging.warning(f"text: unknown align parameters: {align}") lc.translate(position[0], position[1]) return lc
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 render_module_set( img: np.ndarray, mset_path: str, quantization: float, random_mirror: bool, return_sizes: bool = False, ) -> Union[vp.LineCollection, Tuple[vp.LineCollection, float, float]]: """ Build a LineCollection from a 2-dimension bool Numpy array and a path to a module set """ modules, tile_w, tile_h = load_module_set(mset_path, quantization) lc = vp.LineCollection() for idx, mod_id in np.ndenumerate(bitmap_to_module(img)): if mod_id != -1: mod_lc = vp.LineCollection(modules[mod_id]) if random_mirror: mod_lc.translate(-tile_w / 2, -tile_h / 2) if MSET_MIRRORS[mod_id][0] and random.random() < 0.5: mod_lc.scale(-1, 1) if MSET_MIRRORS[mod_id][1] and random.random() < 0.5: mod_lc.scale(1, -1) mod_lc.translate(tile_w / 2, tile_h / 2) mod_lc.translate(idx[1] * tile_w, idx[0] * tile_h) lc.extend(mod_lc) if return_sizes: return lc, tile_w, tile_h else: return lc
def iread(document: vp.Document, input_file: str, color, distance: float): """ Image Read and Vectorization. This is a pure python polygon producer. The goal of this project is to vector trace images according to some given criteria. The default mode does black v. white. However, multiple colors can be specified along with a color distance and those colors will be extracted and traced. """ image = Image.open(input_file) width, height = image.size if len(color) == 0: if image.mode != 'L': image = image.convert('L') image = image.point(lambda e: int(e > 127) * 255) lc = vp.LineCollection() document.add(lc) for points in _vectrace(image.load(), width, height): lc.append(points) return document distance_sq = distance * distance def dist(c, pixel): r = c.red - pixel[0] g = c.green - pixel[1] b = c.blue - pixel[2] return r * r + g * g + b * b <= distance_sq if image.mode != "RGBA": image = image.convert("RGBA") for c in color: v = Image.new('L', image.size, 255) v_data = v.load() new_data = image.load() for y in range(height): for x in range(width): pixel = new_data[x, y] if pixel[3] == 0: continue if dist(c, pixel): new_data[x, y] = (255, 255, 255, 0) v_data[x, y] = 0 lc = vp.LineCollection() document.add(lc) for points in _vectrace(v_data, width, height): lc.append(points) return document
def generate_fill(poly: Polygon, pen_width: float, stroke_width: float) -> vp.LineCollection: """Draw a fill pattern for the the input polygon. The fill pattern should take into account the stroke width. Args: poly: polygon to fill pen_width: pen width on paper stroke_width: width of the stroke (accounting for stroke pen width and weight) Returns: fill paths """ # we draw the boundary, accounting for pen width if stroke_width > 0: p = poly.buffer(-stroke_width / 2, join_style=2, mitre_limit=10.0) else: p = poly if p.is_empty: # too small, nothing to fill return vp.LineCollection() min_x, min_y, max_x, max_y = p.bounds height = max_y - min_y line_count = math.ceil(height / pen_width) + 1 base_seg = np.array([min_x, max_x]) y_start = min_y + (height - (line_count - 1) * pen_width) / 2 segs = [] for n in range(line_count): seg = base_seg + (y_start + pen_width * n) * 1j segs.append(seg if n % 2 == 0 else np.flip(seg)) # mls = MultiLineString([[(pt.real, pt.imag) for pt in seg] for seg in segs]).intersection( mls = MultiLineString([complex_to_2d(seg) for seg in segs]).intersection( p.buffer(-pen_width / 2, join_style=2, mitre_limit=10.0)) lc = vp.LineCollection(mls) lc.merge(tolerance=pen_width * 5, flip=True) boundary = p.boundary if boundary.geom_type == "MultiLineString": lc.extend(boundary) else: lc.append(boundary) return lc
def fracture(size, pitch): """""" width = size[0] height = size[1] count = round(height / pitch + 1) white_start = np.clip( np.random.normal(scale=0.1 * width, size=count) + 0.4 * width, 0.05 * width, 0.65 * width, ) white_stop = np.clip( np.random.normal(scale=0.1 * width, size=count) + 0.6 * width, 0.35 * width, 0.95 * width, ) # avoid any contact between left-hand and right-hand set of lines white_start = np.clip(white_start, a_min=None, a_max=white_stop - 0.05 * width) white_stop = np.clip(white_stop, a_min=white_start + 0.05 * width, a_max=None) white_start[1:] = np.clip( white_start[1:], a_min=None, a_max=white_stop[:-1] - 0.05 * width ) white_stop[1:] = np.clip(white_stop[1:], a_min=white_start[:-1] + 0.05 * width, a_max=None) # generate line collection, taking care of line order and start return vp.LineCollection( [np.array([0, white_start[i]]) + 1j * i * pitch for i in range(count)] + [np.array([width, white_stop[i]]) + 1j * i * pitch for i in range(count)] )
def ellipse(x: float, y: float, w: float, h: float, quantization: float): """Generate lines approximating an ellipse. The ellipse is centered on (X, Y), with a half-width of W and a half-height of H. """ return vp.LineCollection([vp.ellipse(x, y, w, h, quantization)])
def vpype_flow_imager(filename, noise_coeff, n_fields, min_sep, max_sep, min_length, max_length, max_size, seed, flow_seed, search_ef, test_frequency, field_type, transparent_val, edge_field_multiplier, dark_field_multiplier): """ Generate flowline representation from an image. The generated flowlines are in the coordinates of the input image, resized to have dimensions at most `--max_size` pixels. """ gray_img = cv2.imread(filename, cv2.IMREAD_UNCHANGED) with tmp_np_seed(seed): numpy_paths = draw_image( gray_img, mult=noise_coeff, n_fields=n_fields, min_sep=min_sep, max_sep=max_sep, min_length=min_length, max_length=max_length, max_img_size=max_size, flow_seed=flow_seed, search_ef=search_ef, test_frequency=test_frequency, field_type=field_type, transparent_val=transparent_val, edge_field_multiplier=edge_field_multiplier, dark_field_multiplier=dark_field_multiplier, ) lc = vp.LineCollection() for path in numpy_paths: lc.append(path[:, 0] + path[:, 1] * 1.j) return lc
def _generate_fill(poly: Polygon, pen_width: float) -> vp.LineCollection: # nasty hack because unary_union() did something weird once poly = Polygon(poly.exterior) # we draw the boundary, accounting for pen width p = poly.buffer(-pen_width / 2) min_x, min_y, max_x, max_y = p.bounds height = max_y - min_y line_count = math.ceil(height / pen_width) + 1 base_seg = np.array([min_x, max_x]) y_start = min_y + (height - (line_count - 1) * pen_width) / 2 segs = [] for n in range(line_count): seg = base_seg + (y_start + pen_width * n) * 1j segs.append(seg if n % 2 == 0 else np.flip(seg)) mls = MultiLineString([[(pt.real, pt.imag) for pt in seg] for seg in segs ]).intersection(p.buffer(-pen_width / 2)) lc = vp.LineCollection(mls) lc.merge(tolerance=pen_width * 5, flip=True) print(p.geom_type) boundary = p.boundary if boundary.geom_type == "MultiLineString": lc.extend(boundary) else: lc.append(boundary) return lc
def msfingerprint(mset, quantization, fingerprint): """Generate geometries based on a previously generated fingerprint.""" parts = fingerprint.split("_") if len(parts) < 3 or len(parts) > 4: logging.warning(f"msfingerprint: invalid fingerprint {fingerprint}") return vp.LineCollection() size_x = int(parts[0]) size_y = int(parts[1]) data = bytearray.fromhex(parts[2]) if len(parts) == 4: seed = int(parts[3], 16) else: seed = None img = (np.unpackbits(np.array(data), count=size_x * size_y) == 1).reshape( (size_y, size_x)) if seed is not None: random.seed(seed) return render_module_set(img, mset, quantization, random_mirror=seed is not None)
def render(self): n = self.width() / self.unit_length t = np.linspace(0, n * 2 * math.pi, int(n * 50)) return vp.LineCollection([ self.elem_to_global_path(t / t[-1] * self.width() + 1j * (self.dr / 2 + np.sin(t) * self.dr / 2)) ])
def linesort(lines: vp.LineCollection, no_flip: bool = True): """ Sort lines to minimize the pen-up travel distance. Note: this process can be lengthy depending on the total number of line. Consider using `linemerge` before `linesort` to reduce the total number of line and thus significantly optimizing the overall plotting time. """ if len(lines) < 2: return lines index = vp.LineIndex(lines[1:], reverse=not no_flip) new_lines = vp.LineCollection([lines[0]]) while len(index) > 0: idx, reverse = index.find_nearest(new_lines[-1][-1]) line = index.pop(idx) if reverse: line = np.flip(line) new_lines.append(line) logging.info( f"optimize: reduced pen-up (distance, mean, median) from {lines.pen_up_length()} to " f"{new_lines.pen_up_length()}" ) return new_lines
def rect(x: float, y: float, width: float, height: float) -> vp.LineCollection: """ Generate a rectangle. The rectangle is defined by its top left corner (X, Y) and its width and height. """ return vp.LineCollection([vp.rect(x, y, width, height)])
def rect( x: float, y: float, width: float, height: float, radii: Tuple[float, float, float, float], quantization: float, ) -> vp.LineCollection: """Generate a rectangle, with optional rounded angles. The rectangle is defined by its top left corner (X, Y) and its width and height. Examples: Straight-angle rectangle: vpype rect 10cm 10cm 3cm 2cm show Rounded-angle rectangle: vpype rect --radii 5mm 5mm 5mm 5mm 10cm 10cm 3cm 2cm show Rounded-angle rectangle with quantization control: vpype rect --quantization 0.1mm --radii 5mm 5mm 5mm 5mm 10cm 10cm 3cm 2cm show """ return vp.LineCollection( [vp.rect(x, y, width, height, *radii, quantization)])
def circle(x: float, y: float, r: float, quantization: float): """Generate lines approximating a circle. The circle is centered on (X, Y) and has a radius of R. """ return vp.LineCollection([vp.circle(x, y, r, quantization)])
def line(x0: float, y0: float, x1: float, y1: float) -> vp.LineCollection: """ Generate a single line. The line starts at (X0, Y0) and ends at (X1, Y1). All arguments understand supported units. """ return vp.LineCollection([vp.line(x0, y0, x1, y1)])
def render(self): w = self.width() lc = vp.LineCollection([ np.linspace(h * 1j, w + h * 1j, math.ceil(w / self.quantization)) for h in np.linspace(0, self.dr, self.line_count) ]) return self.elem_to_global_lc(lc)
def render(self): w = self.width() n = math.ceil(w / self.unit_length) lc = vp.LineCollection() lc.extend([ 1j * self.dr / 2 + np.linspace( (i + 0.1) * w / n, (i + 0.9) * w / n, int(w / self.quantization)) for i in range(n) ]) lc.extend([ np.array([0.1, 0.4]) * 1j * self.dr + i * w / n for i in range(1, n) ]) lc.extend([ np.array([0.6, 0.9]) * 1j * self.dr + i * w / n for i in range(1, n) ]) lc.extend([ vp.circle(i * w / n, self.dr / 2, DOT_RADIUS, DOT_RADIUS / 15) for i in range(1, n) ]) return self.elem_to_global_lc(lc)
def snap(line_collection: vp.LineCollection, pitch: float) -> vp.LineCollection: """Snap all points to a grid with with a spacing of PITCH. This command moves every point of every paths to the nearest grid point. If sequential points of a segment end up on the same grid point, they are deduplicated. If the resulting path contains less than 2 points, it is discarded. Example: Snap all points to a grid of 3x3mm: vpype [...] snap 3mm [...] """ line_collection.scale(1 / pitch) new_lines = vp.LineCollection() for line in line_collection: new_line = np.round(line) idx = np.ones(len(new_line), dtype=bool) idx[1:] = new_line[1:] != new_line[:-1] if idx.sum() > 1: new_lines.append(np.round(new_line[idx])) new_lines.scale(pitch) return new_lines
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 render(self): w = self.width() lc = vp.LineCollection([ np.linspace(0, w, math.ceil(w / self.quantization)) + 1j * self.dr / 2 ]) return self.elem_to_global_lc(lc)
def execute_single_line(pipeline: str, line: vp.LineLike) -> vp.LineCollection: """Execute a pipeline on a single line. The pipeline is expected to remain single layer. Returns: the layer 1's LineCollection """ doc = execute(pipeline, vp.Document(vp.LineCollection([line]))) assert len(doc.layers) == 1 return doc.layers[1]
def render(self): w = self.width() n = math.ceil(w / self.unit_length) x = np.hstack([[0, 0], np.arange(0.5, n - 0.5, 0.5), n - 1]) * w / n y = self.dr * np.ones(x.shape) y[1::2] = 0 return vp.LineCollection([self.elem_to_global_path(x + 1j * y)])
def big_doc(): random.seed(0) doc = vp.Document() doc.add( vp.LineCollection([( random.uniform(0, 100) + random.uniform(0, 100) * 1j, random.uniform(0, 100) + random.uniform(0, 100) * 1j, ) for _ in range(1000)])) return doc
def arc(x: float, y: float, rw: float, rh: float, start: float, stop: float, quantization: float): """Generate lines approximating a circular arc. The arc is centered on (X, Y) and has a radius of R and spans counter-clockwise from START to STOP angles (in degrees). Angular values of zero refer to east of unit circle and positive values extend counter-clockwise. """ return vp.LineCollection([vp.arc(x, y, rw, rh, start, stop, quantization)])
def __post_init__(self): super().__post_init__() lines = axi.text(self.text, self.font) self.item_lc = vp.LineCollection() for line in lines: self.item_lc.append([x + 1j * y for x, y in line]) x1, y1, x2, y2 = self.item_lc.bounds() self.item_lc.translate(-(x1 + x2) / 2, -(y1 + y2) / 2) s = self.unit_length / (y2 - y1) self.item_lc.scale(s, -s)
def vpype_flow_imager(document, layer, filename, noise_coeff, n_fields, min_sep, max_sep, min_length, max_length, max_size, seed, flow_seed, search_ef, test_frequency, field_type, transparent_val, transparent_mask, edge_field_multiplier, dark_field_multiplier, kdtree_searcher, cmyk): """ Generate flowline representation from an image. The generated flowlines are in the coordinates of the input image, resized to have dimensions at most `--max_size` pixels. """ if kdtree_searcher: searcher_class = KDTSearcher else: searcher_class = HNSWSearcher target_layer = vp.single_to_layer_id(layer, document) img = cv2.imread(filename, cv2.IMREAD_UNCHANGED) logger.debug(f"original img.shape: {img.shape}") with tmp_np_seed(seed): if cmyk: img_layers = split_cmyk(img.copy()) else: img_layers = [img] alpha = get_alpha_channel(img) for layer_i, img_layer in enumerate(img_layers): logger.info(f"computing layer {layer_i+1}") numpy_paths = draw_image( img_layer, alpha, mult=noise_coeff, n_fields=n_fields, min_sep=min_sep, max_sep=max_sep, min_length=min_length, max_length=max_length, max_img_size=max_size, flow_seed=flow_seed, search_ef=search_ef, test_frequency=test_frequency, field_type=field_type, transparent_val=transparent_val, transparent_mask=transparent_mask, edge_field_multiplier=edge_field_multiplier, dark_field_multiplier=dark_field_multiplier, searcher_class=searcher_class, ) lc = vp.LineCollection() for path in numpy_paths: lc.append(path[:, 0] + path[:, 1] * 1.j) document.add(lc, target_layer + layer_i) return document
def render(self): w = self.width() n = max(math.ceil(self.dr / self.quantization), 2) m = max(math.ceil(w / self.quantization), 2) # noinspection PyTypeChecker p = np.hstack([ np.linspace(0, self.dr * 1j, n), np.linspace(self.dr * 1j, self.dr * 1j + w, m), np.linspace(self.dr * 1j + w, w, n), np.linspace(w, 0, m, dtype=complex), ]) return vp.LineCollection([self.elem_to_global_path(p)])
def generate_fill(rect: RectType, pen_width: float) -> vp.LineCollection: line_count = math.ceil(rect[3] / pen_width) base_seg = np.array([pen_width / 2, rect[2] - pen_width / 2]) + rect[0] y_start = rect[1] + (rect[3] - (line_count - 1) * pen_width) / 2 segs = [] for n in range(line_count): seg = base_seg + (y_start + pen_width * n) * 1j segs.append(seg if n % 2 == 0 else np.flip(seg)) return vp.LineCollection([np.hstack(segs)])
def render(self) -> vp.LineCollection: w = self.width() n = math.ceil(w / self.unit_length) lc = vp.LineCollection() for i in range(n): x, y = self.elem_to_global_coord((i / n) * w, self.dr / 2) item_lc = self.render_item(i, n) angle = self.start_angle + (i / n) * (self.stop_angle - self.start_angle) + 90.0 item_lc.rotate(-angle * math.pi / 180.0) item_lc.translate(x, y) lc.extend(item_lc) return lc