def process(self, processors): vector_data = VectorData() for _ in range(self.number): state = execute_processors(processors) vector_data.extend(state.vector_data) return vector_data
def process(self, processors): vector_data = VectorData() for i in range(self.number[0]): for j in range(self.number[1]): state = execute_processors(processors) state.vector_data.translate(self.offset[0] * i, self.offset[1] * j) vector_data.extend(state.vector_data) return vector_data
def scale( vector_data: VectorData, scale: Tuple[float, float], layer: Union[int, List[int]], absolute: bool, keep_proportions: bool, origin_coords: Tuple[float, float], ): """Scale the geometries. The origin used is the bounding box center, unless the `--origin` option is used. By default, the arguments are used as relative factors (e.g. `scale 2 2` make the geometries twice as big in both dimensions). With `--to`, the arguments are interpreted as the final size. In this case, arguments understand the supported units (e.g. `scale --to 10cm 10cm`). By default, act on all layers. If one or more layer IDs are provided with the `--layer` option, only these layers will be affected. In this case, the bounding box is that of the listed layers. """ if vector_data.is_empty(): return vector_data # these are the layers we want to act on layer_ids = multiple_to_layer_ids(layer, vector_data) bounds = vector_data.bounds(layer_ids) if absolute: factors = (scale[0] / (bounds[2] - bounds[0]), scale[1] / (bounds[3] - bounds[1])) if keep_proportions: factors = (min(factors), min(factors)) else: factors = scale if len(origin_coords) == 2: origin = origin_coords else: origin = ( 0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3]), ) logging.info(f"scaling factors: {factors}, origin: {origin}") for vid in layer_ids: lc = vector_data[vid] lc.translate(-origin[0], -origin[1]) lc.scale(factors[0], factors[1]) lc.translate(origin[0], origin[1]) return vector_data
def ldelete(vector_data: VectorData, layers) -> VectorData: """Delete one or more layers. LAYERS can be a single layer ID, the string 'all' (to delete all layers), or a coma-separated, whitespace-free list of layer IDs. """ lids = set(multiple_to_layer_ids(layers, vector_data)) new_vector_data = VectorData() for lid in vector_data.ids(): if lid not in lids: new_vector_data[lid] = vector_data[lid] return new_vector_data
def write( vector_data: VectorData, cmd_string: str, output, single_path: bool, page_format: Tuple[float, float], landscape: bool, center: bool, layer_label: str, ): """Write command.""" if vector_data.is_empty(): logging.warning("no geometry to save, no file created") else: if landscape: page_format = page_format[::-1] write_svg( output=output, vector_data=vector_data, page_format=page_format, center=center, source_string=cmd_string, layer_label_format=layer_label, single_path=single_path, ) return vector_data
def trim(vector_data: VectorData, margin_x: float, margin_y: float, layer: Union[int, List[int]]) -> VectorData: """Trim the geometries by some margin. This command trims the geometries by the provided X and Y margins with respect to the current bounding box. By default, `trim` acts on all layers. If one or more layer IDs are provided with the `--layer` option, only these layers will be affected. In this case, the bounding box is that of the listed layers. """ layer_ids = multiple_to_layer_ids(layer, vector_data) bounds = vector_data.bounds(layer_ids) if not bounds: return vector_data min_x = bounds[0] + margin_x max_x = bounds[2] - margin_x min_y = bounds[1] + margin_y max_y = bounds[3] - margin_y if min_x > max_x: min_x = max_x = 0.5 * (min_x + max_x) if min_y > max_y: min_y = max_y = 0.5 * (min_y + max_y) for vid in layer_ids: lc = vector_data[vid] lc.crop(min_x, min_y, max_x, max_y) return vector_data
def test_vector_data_bounds_empty_layer(): vd = VectorData() vd.add(LineCollection([(0, 10 + 10j)]), 1) vd.add(LineCollection()) assert vd.bounds() == (0, 0, 10, 10)
def rotate( vector_data: VectorData, angle: float, layer: Union[int, List[int]], radian: bool, origin_coords: Tuple[float, float], ): """ Rotate the geometries (clockwise positive). The origin used is the bounding box center, unless the `--origin` option is used. By default, act on all layers. If one or more layer IDs are provided with the `--layer` option, only these layers will be affected. In this case, the bounding box is that of the listed layers. """ if vector_data.is_empty(): return vector_data if not radian: angle *= math.pi / 180.0 # these are the layers we want to act on layer_ids = multiple_to_layer_ids(layer, vector_data) bounds = vector_data.bounds(layer_ids) if len(origin_coords) == 2: origin = origin_coords else: origin = ( 0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3]), ) logging.info(f"rotating origin: {origin}") for vid in layer_ids: lc = vector_data[vid] lc.translate(-origin[0], -origin[1]) lc.rotate(angle) lc.translate(origin[0], origin[1]) return vector_data
def skew( vector_data: VectorData, layer: Union[int, List[int]], angles: Tuple[float, float], radian: bool, origin_coords: Tuple[float, float], ): """ Skew the geometries. The geometries are sheared by the provided angles along X and Y dimensions. The origin used in the bounding box center, unless the `--centroid` or `--origin` options are used. """ if vector_data.is_empty(): return vector_data # these are the layers we want to act on layer_ids = multiple_to_layer_ids(layer, vector_data) bounds = vector_data.bounds(layer_ids) if len(origin_coords) == 2: origin = origin_coords else: origin = ( 0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3]), ) if not radian: angles = tuple(a * math.pi / 180.0 for a in angles) logging.info(f"skewing origin: {origin}") for vid in layer_ids: lc = vector_data[vid] lc.translate(-origin[0], -origin[1]) lc.skew(angles[0], angles[1]) lc.translate(origin[0], origin[1]) return vector_data
def __init__(self, data: Dict[str, Any]): self.count = data["count"] self.length = data.get("length", 0) self.pen_up_length = data.get("pen_up_length", 0) self.bounds = data.get("bounds", [0, 0, 0, 0]) self.layers = data.get("layers", {}) self.vector_data = VectorData() for vid, lines in self.layers.items(): self.vector_data[int(vid)] = LineCollection( [np.array([x + 1j * y for x, y in line]) for line in lines])
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 test_vector_data_lid_iteration(): lc = LineCollection([(0, 1 + 1j)]) vd = VectorData() vd.add(lc, 1) for lc in vd.layers_from_ids([1, 2, 3, 4]): lc.append([3, 3 + 3j]) assert vd.count() == 1 assert len(vd.layers[1]) == 2
def _compute_origin( vector_data: VectorData, layer: Optional[Union[int, List[int]]], origin_coords: Union[Tuple[()], Tuple[float, float]], ) -> Tuple[Tuple[float, float], List[int], Tuple[float, float, float, float]]: layer_ids = multiple_to_layer_ids(layer, vector_data) bounds = vector_data.bounds(layer_ids) if not bounds: logging.warning("no geometry available, cannot compute origin") raise ValueError if len(origin_coords) == 2: origin = origin_coords else: origin = ( 0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3]), ) return cast(Tuple[float, float], origin), layer_ids, bounds
def read( vector_data: VectorData, file, single_layer: bool, layer: Optional[int], quantization: float, no_simplify: bool, ) -> VectorData: """Extract geometries from a SVG file. By default, the `read` command attempts to preserve the layer structure of the SVG. In this context, top-level groups (<svg:g>) are each considered a layer. If any, all non-group, top-level SVG elements are imported into layer 1. The following logic is used to determine in which layer each SVG top-level group is imported: - If a `inkscape:label` attribute is present and contains digit characters, it is stripped of non-digit characters the resulting number is used as target layer. If the resulting number is 0, layer 1 is used instead. - If the previous step fails, the same logic is applied to the `id` attribute. - If both previous steps fail, the target layer matches the top-level group's order of appearance. Using `--single-layer`, the `read` command operates in single-layer mode. In this mode, all geometries are in a single layer regardless of the group structure. The current target layer is used default and can be specified with the `--layer` option. This command only extracts path elements as well as primitives (rectangles, ellipses, lines, polylines, polygons). Other elements such as text and bitmap images are discarded, and so is all formatting. All curved primitives (e.g. bezier path, ellipses, etc.) are linearized and approximated by polylines. The quantization length controls the maximum length of individual segments. By default, an implicit line simplification with tolerance set to quantization is executed (see `linesimplify` command). This behaviour can be disabled with the `--no-simplify` flag. Examples: Multi-layer import: vpype read input_file.svg [...] Single-layer import: vpype read --single-layer input_file.svg [...] Single-layer import with target layer: vpype read --single-layer --layer 3 input_file.svg [...] Multi-layer import with specified quantization and line simplification disabled: vpype read --quantization 0.01mm --no-simplify input_file.svg [...] """ if single_layer: vector_data.add( read_svg(file, quantization=quantization, simplify=not no_simplify), single_to_layer_id(layer, vector_data), ) else: if layer is not None: logging.warning( "read: target layer is ignored in multi-layer mode") vector_data.extend( read_multilayer_svg(file, quantization=quantization, simplify=not no_simplify)) return vector_data
def _all_vector_data_ops(vd: VectorData): vd.bounds() vd.length() vd.segment_count()
def write( vector_data: VectorData, cmd_string: Optional[str], output, file_format: str, page_format: str, landscape: bool, center: bool, layer_label: str, pen_up: bool, color_mode: str, single_path: bool, device: Optional[str], velocity: Optional[int], quiet: bool, ): """Write command.""" if vector_data.is_empty(): logging.warning("no geometry to save, no file created") return vector_data if file_format is None: # infer format _, ext = os.path.splitext(output.name) file_format = ext.lstrip(".").lower() if file_format == "svg": page_format_px = convert_page_format(page_format) if landscape: page_format_px = page_format_px[::-1] write_svg( output=output, vector_data=vector_data, page_format=page_format_px, center=center, source_string=cmd_string if cmd_string is not None else "", layer_label_format=layer_label, single_path=single_path, show_pen_up=pen_up, color_mode=color_mode, ) elif file_format == "hpgl": write_hpgl( output=output, vector_data=vector_data, landscape=landscape, center=center, device=device, page_format=page_format, velocity=velocity, quiet=quiet, ) else: logging.warning( f"write: format could not be inferred or format unknown '{file_format}', " "no file created" ) return vector_data
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 test_vector_data_bounds(): vd = VectorData() vd.add(LineCollection([(-10, 10), (0, 0)]), 1) vd.add(LineCollection([(0, 0), (-10j, 10j)]), 2) assert vd.bounds() == (-10, -10, 10, 10)
def test_ops_on_emtpy_vector_data(): vd = VectorData() _all_vector_data_ops(vd)
def test_ops_on_vector_data_with_emtpy_layer(): vd = VectorData() lc = LineCollection() vd.add(lc, 1) _all_vector_data_ops(vd)