Exemple #1
0
    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
Exemple #2
0
    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
Exemple #3
0
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
Exemple #4
0
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
Exemple #5
0
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
Exemple #6
0
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
Exemple #7
0
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)
Exemple #8
0
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
Exemple #9
0
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
Exemple #10
0
    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])
Exemple #11
0
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
Exemple #12
0
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
Exemple #13
0
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
Exemple #14
0
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
Exemple #15
0
def _all_vector_data_ops(vd: VectorData):
    vd.bounds()
    vd.length()
    vd.segment_count()
Exemple #16
0
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
Exemple #17
0
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
Exemple #18
0
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)
Exemple #19
0
def test_ops_on_emtpy_vector_data():
    vd = VectorData()
    _all_vector_data_ops(vd)
Exemple #20
0
def test_ops_on_vector_data_with_emtpy_layer():
    vd = VectorData()
    lc = LineCollection()
    vd.add(lc, 1)
    _all_vector_data_ops(vd)