Example #1
0
def ldelete(document: Document, layers, prob: Optional[float]) -> Document:
    """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.

    The `--prob` option controls the probability with which each path is deleted. With a value
    lower than 1.0, some paths will not be deleted.
    """

    lids = set(multiple_to_layer_ids(layers, document))

    for lid in lids:
        if prob is not None:
            lc = LineCollection()
            for line in document[lid]:
                if not random.random() < prob:
                    lc.append(line)

            if len(lc) == 0:
                document.pop(lid)
            else:
                document[lid] = lc
        else:
            document.pop(lid)

    return document
Example #2
0
def lmove(vector_data, sources, dest):
    """Move the content of one or more layer(s) to another layer.

    SOURCES can be a single layer ID, the string 'all' (to copy all non-empty layers,
    or a coma-separated, whitespace-free list of layer IDs.

    DEST can be a layer ID or the string 'new', in which case a new layer with the
    lowest available ID will be created.

    Layer(s) left empty after the move are then discarded and may thus be reused by subsequent
    commands using 'new' as destination layer.

    If a layer is both in the source and destination, its content is not duplicated.

    Examples:

        Merge layer 1 and 2 to layer 1 (the content of layer 1 is not duplicated):

            vpype [...] lmove 1,2 1 [...]  # merge layer 1 and 2 to layer 1
    """

    src_lids = multiple_to_layer_ids(sources, vector_data)
    dest_lid = single_to_layer_id(dest, vector_data)

    if dest_lid in src_lids:
        src_lids.remove(dest_lid)

    for lid in src_lids:
        vector_data.add(vector_data.pop(lid), dest_lid)

    return vector_data
Example #3
0
def trim(document: vp.Document, margin_x: float, margin_y: float,
         layer: Union[int, List[int]]) -> vp.Document:
    """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 = vp.multiple_to_layer_ids(layer, document)
    bounds = document.bounds(layer_ids)

    if not bounds:
        return document

    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 = document[vid]
        lc.crop(min_x, min_y, max_x, max_y)

    return document
Example #4
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
Example #5
0
def lmove(document, sources, dest, prob: Optional[float]):
    """Move the content of one or more layer(s) to another layer.

    SOURCES can be a single layer ID, the string 'all' (to copy all non-empty layers,
    or a coma-separated, whitespace-free list of layer IDs.

    DEST can be a layer ID or the string 'new', in which case a new layer with the
    lowest available ID will be created.

    Layer(s) left empty after the move are then discarded and may thus be reused by subsequent
    commands using 'new' as destination layer.

    The `--prob` option controls the probability with which each path is moved. With a value
    lower than 1.0, some paths will not be moved to DEST, which may be used to achieve random
    coloring effects.

    If a layer is both in the source and destination, its content is not duplicated.

    Examples:

        Merge layer 1 and 2 to layer 1 (the content of layer 1 is not duplicated):

            vpype [...] lmove 1,2 1 [...]  # merge layer 1 and 2 to layer 1
    """

    src_lids = multiple_to_layer_ids(sources, document)
    dest_lid = single_to_layer_id(dest, document)

    if dest_lid in src_lids:
        src_lids.remove(dest_lid)

    for lid in src_lids:
        if prob is not None:
            # split lines with provided probability
            remaining_lines = LineCollection()
            moving_lines = LineCollection()
            for line in document.layers[lid]:
                if random.random() < prob:
                    moving_lines.append(line)
                else:
                    remaining_lines.append(line)

            if len(remaining_lines) > 0:
                document.layers[lid] = remaining_lines
            else:
                document.pop(lid)

            if len(moving_lines) > 0:
                document.add(moving_lines, dest_lid)
        else:
            document.add(document.pop(lid), dest_lid)

    return document
Example #6
0
def lcopy(document, sources, dest, prob: Optional[float]):
    """Copy the content of one or more layer(s) to another layer.

    SOURCES can be a single layer ID, the string 'all' (to copy all non-empty layers,
    or a coma-separated, whitespace-free list of layer IDs.

    DEST can be a layer ID or the string 'new', in which case a new layer with the
    lowest available ID will be created.

    If a layer is both in the source and destination, its content is not duplicated.

    The `--prob` option controls the probability with which each path is copied. With a value
    lower than 1.0, some paths will not be copied to DEST, which may be used to achieve random
    coloring effects.

    Examples:

        Copy layer 1 to a new layer:

            vpype [...] lcopy 1 new [...]  # duplicate layer 1

        Make a new layer with a merged copy of layer 1 and 2:

            vpype [...] lcopy 1,2 new [...]  # make new layer with merged copy of layer 1 and 2

        Add a merged copy of all layers to layer 1. If layer 1 previously had content, this \
content is not duplicated:

            vpype [...] lcopy all 1 [...]
    """

    src_lids = multiple_to_layer_ids(sources, document)
    dest_lid = single_to_layer_id(dest, document)

    if dest_lid in src_lids:
        src_lids.remove(dest_lid)

    lc = LineCollection()
    for lid in src_lids:
        if prob is not None:
            for line in document[lid]:
                if random.random() < prob:
                    lc.append(line)
        else:
            lc.extend(document[lid])

    if len(lc) > 0:
        document.add(lc, dest_lid)

    return document
Example #7
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
Example #8
0
def ldelete(document: Document, layers) -> Document:
    """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, document))

    new_doc = document.empty_copy()
    for lid in document.ids():
        if lid not in lids:
            new_doc[lid] = document[lid]

    return new_doc
Example #9
0
def occult(
    document: vpype.Document,
    tolerance: float,
    layer: Optional[Union[int, List[int]]],
    keep_occulted: bool = False,
) -> vpype.Document:
    """
    Remove lines occulted by polygons.
    The 'keep_occulted' option (-k, --keep-occulted) saves removed geometries in a new layer.
    The order of the geometries in 'lines' matters, see basic example below.
    Occlusion is performed layer by layer. This means that if one geometry is occulting another,
    and these geometries are not in the same layer, occult won't remove occulted paths.

    Args:
        document: the vpype.Document to work on.
        tolerance: controls the distance tolerance between the first and last points
        of a geometry to consider it closed.
        layer: specify which layer(s) to work on. Default: all.
        keep_occulted: If set, this flag allows to save removed lines in a separate layer.

    Examples:

        - Basic usage:
            $ vpype line 0 0 5cm 5cm rect 2cm 2cm 1cm 1cm occult show  # line is occulted by rect
            $ vpype rect 2cm 2cm 1cm 1cm line 0 0 5cm 5cm occult show  # line is NOT occulted by rect,
            as the line is drawn after the rectangle.

        - Keep occulted lines in a separate layer:
            $ vpype line -- -3cm 0 8cm 0  circle 0 0 3cm  circle -l 2 5cm 0 3cm occult -k show
            # 'occult -k' will remove the path inside the first circle, and put it in a third layer.
            # both the first circle and the line are not affected by the second circle, as it is
            # in a different layer.
    """

    new_document = document.empty_copy()
    layer_ids = multiple_to_layer_ids(layer, document)
    removed_layer_id = document.free_id()

    for lines, l_id in zip(document.layers_from_ids(layer_ids), layer_ids):
        lines, removed_lines = _occult_layer(lines, tolerance, keep_occulted)
        new_document.add(lines, layer_id=l_id)

        if keep_occulted and not removed_lines.is_empty():
            new_document.add(removed_lines, layer_id=removed_layer_id)

    return new_document
Example #10
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
Example #11
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
Example #12
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
Example #13
0
def _compute_origin(
    document: vp.Document,
    layer: Optional[Union[int, List[int]]],
    origin_coords: Optional[Union[Tuple[()], Tuple[float, float]]],
) -> Tuple[Tuple[float, float], List[int], Tuple[float, float, float, float]]:
    layer_ids = vp.multiple_to_layer_ids(layer, document)
    bounds = document.bounds(layer_ids)

    if not bounds:
        logging.warning("no geometry available, cannot compute origin")
        raise ValueError

    if origin_coords is not None and 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
Example #14
0
def lcopy(vector_data, sources, dest):
    """Copy the content of one or more layer(s) to another layer.

    SOURCES can be a single layer ID, the string 'all' (to copy all non-empty layers,
    or a coma-separated, whitespace-free list of layer IDs.

    DEST can be a layer ID or the string 'new', in which case a new layer with the
    lowest available ID will be created.

    If a layer is both in the source and destination, its content is not duplicated.

    Examples:

        Copy layer 1 to a new layer:

            vpype [...] lcopy 1 new [...]  # duplicate layer 1

        Make a new layer with a merged copy of layer 1 and 2:

            vpype [...] lcopy 1,2 new [...]  # make new layer with merged copy of layer 1 and 2

        Add a merged copy of all layers to layer 1. If layer 1 previously had content, this
content is not duplicated:

            vpype [...] lcopy all 1 [...]
    """

    src_lids = multiple_to_layer_ids(sources, vector_data)
    dest_lid = single_to_layer_id(dest, vector_data)

    if dest_lid in src_lids:
        src_lids.remove(dest_lid)

    lc = LineCollection()
    for lid in src_lids:
        lc.extend(vector_data[lid])
    vector_data.add(lc, dest_lid)

    return vector_data
Example #15
0
def occult(
    document: vpype.Document,
    tolerance: float,
    layer: Optional[Union[int, List[int]]],
    keep_occulted: bool = False,
    ignore_layers: bool = False,
    reverse: bool = False,
) -> vpype.Document:
    """
    Remove lines occulted by polygons.
    The 'keep_occulted' option (-k, --keep-occulted) saves removed geometries in a new layer.
    The order of the geometries in 'lines' matters, see basic example below.
    Occlusion is performed layer by layer. This means that if one geometry is occulting another,
    and these geometries are not in the same layer, occult won't remove occulted paths.
    With the 'ignore_layers' option, occlusion is performed on all geometry regardless
    of layers, with higher-numbered layers occluding lower-numbered layers.

    Args:
        document: the vpype.Document to work on.
        tolerance: controls the distance tolerance between the first and last points
        of a geometry to consider it closed.
        layer: specify which layer(s) to work on. Default: all.
        keep_occulted: If set, this flag allows to save removed lines in a separate layer.
        ignore_layers: If set, this flag causes occult to treat all geometries as if they
        exist on the same layer. However, all geometries in the final result
        remain on their original layer.

    Examples:

        - Basic usage:
            $ vpype line 0 0 5cm 5cm rect 2cm 2cm 1cm 1cm occult show  # line is occulted by rect
            $ vpype rect 2cm 2cm 1cm 1cm line 0 0 5cm 5cm occult show  # line is NOT occulted by rect,
            as the line is drawn after the rectangle.

        - Keep occulted lines in a separate layer:
            $ vpype line -- -3cm 0 8cm 0  circle 0 0 3cm  circle -l 2 5cm 0 3cm occult -k show
            # 'occult -k' will remove the path inside the first circle, and put it in a third layer.
            # both the first circle and the line are not affected by the second circle, as it is
            # in a different layer.
    """
    new_document = document.empty_copy()
    layer_ids = multiple_to_layer_ids(layer, document)
    removed_layer_id = document.free_id()

    if ignore_layers:
        layers = [{
            l_id: list(document.layers_from_ids([l_id]))[0]
            for l_id in layer_ids
        }]
    else:
        layers = [{
            l_id: list(document.layers_from_ids([l_id]))[0]
        } for l_id in layer_ids]

    if reverse:
        for layer in layers:
            for key in layer:
                layer[key].reverse()

    for layer in layers:
        lines, removed_lines = _occult_layer(layer, tolerance, keep_occulted)

        for l_id, occulted_lines in lines.items():
            new_document.add(occulted_lines, layer_id=l_id)

        if keep_occulted and not removed_lines.is_empty():
            new_document.add(removed_lines, layer_id=removed_layer_id)

    return new_document