Beispiel #1
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
Beispiel #2
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)
Beispiel #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
Beispiel #4
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
Beispiel #5
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
Beispiel #6
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
Beispiel #7
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
Beispiel #8
0
def _all_vector_data_ops(vd: VectorData):
    vd.bounds()
    vd.length()
    vd.segment_count()
Beispiel #9
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)
Beispiel #10
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