예제 #1
0
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
예제 #2
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
예제 #3
0
def test_line_collection_pen_up_trajectories():
    lc = LineCollection([(0, 100j, 1000, 10), (5j, 3, 25j),
                         (3 + 3j, 100, 10j)])
    pen_up = lc.pen_up_trajectories()
    assert len(pen_up) == 2
    assert line_collection_contains(pen_up, [10, 5j])
    assert line_collection_contains(pen_up, [25j, 3 + 3j])
예제 #4
0
파일: transforms.py 프로젝트: zxsq-cc/vpype
def translate(lc: LineCollection, offset: Tuple[float, float]):
    """
    Translate the geometries. X and Y offsets must be provided. These arguments understand
    supported units.
    """
    lc.translate(offset[0], offset[1])
    return lc
예제 #5
0
def test_document_bounds_empty_layer():
    doc = Document()

    doc.add(LineCollection([(0, 10 + 10j)]), 1)
    doc.add(LineCollection())

    assert doc.bounds() == (0, 0, 10, 10)
예제 #6
0
def linesort(lines: 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 = LineIndex(lines[1:], reverse=not no_flip)
    new_lines = 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
예제 #7
0
def test_line_collection_extend_two_lines(lines):
    lc = LineCollection([(3, 3j)])
    lc.extend(lines)
    assert len(lc) == 3
    assert np.all(lc[0] == np.array([3, 3j]))
    assert np.all(lc[1] == np.array([0, 1 + 1j]))
    assert np.all(lc[2] == np.array([2 + 2j, 3 + 3j, 4 + 4j]))
예제 #8
0
def filter_command(
    lines: vp.LineCollection,
    min_length: Optional[float],
    max_length: Optional[float],
    closed: bool,
    not_closed: bool,
    tolerance: float,
) -> vp.LineCollection:
    """Filter paths according to specified criterion.

    When an option is provided (e.g. `--min-length 10cm`) the corresponding criterion is
    applied and paths which do not respect the criterion (e.g. a 9cm-long path) are rejected.

    If multiple options are provided, paths will be kept only if they respect every
    corresponding criterion (i.e. logical AND operator).
    """
    keys = []
    if min_length is not None:
        keys.append(
            lambda line: vp.line_length(line) >= cast(float, min_length))
    if max_length is not None:
        keys.append(
            lambda line: vp.line_length(line) <= cast(float, max_length))
    if closed:
        keys.append(lambda line: vp.is_closed(line, tolerance))
    if not_closed:
        keys.append(lambda line: not vp.is_closed(line, tolerance))

    if keys:
        lines.filter(lambda line: vp.union(line, keys))
    else:
        logging.warning(
            "filter: no criterion was provided, all geometries are preserved")

    return lines
예제 #9
0
파일: test_model.py 프로젝트: mfreyre/vpype
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)
예제 #10
0
def test_extend_same_as_init(lines):
    lc1 = LineCollection(lines)
    lc2 = LineCollection()
    lc2.extend(lines)

    assert len(lc1) == len(lc2)
    for l1, l2 in zip(lc1, lc2):
        assert np.all(l1 == l2)
예제 #11
0
def _layout_line_collections(lc_map: Dict[Any, LineCollection], col_count: int,
                             offset: Tuple[float, float]) -> LineCollection:
    lc = LineCollection()
    for i, (key, line) in enumerate(lc_map.items()):
        lc.append(line + (i % col_count) * offset[0] +
                  math.floor(i / col_count) * offset[1] * 1j)

    return lc
예제 #12
0
파일: operations.py 프로젝트: mfreyre/vpype
def reloop(lines: LineCollection, tolerance):
    """
    Randomize the seam location for closed paths. Paths are considered closed when their
    beginning and end points are closer than the provided tolerance.
    """

    lines.reloop(tolerance=tolerance)
    return lines
예제 #13
0
def _occult_layer(
    layers: Dict[int, LineCollection],
    tolerance: float,
    keep_occulted: bool = False
) -> Tuple[Dict[int, LineCollection], LineCollection]:
    """
    Perform occlusion on all provided layers. Optionally returns occulted lines
    in a separate LineCollection.

    Args:
        layers: dictionary of LineCollections to perform occlusion on, keyed by layer ID
        tolerance: Max distance between start and end point to consider a path closed
        keep_occulted: if True, save removed lines in removed_lines LineCollection.
        Otherwise, removed_lines is an empty LineCollection.

    Returns:
        a tuple with two items:
        - new_lines, a dictionary of LineCollections for each layer ID received
        - removed_lines, a LineCollection of removed lines
    """
    removed_lines = LineCollection()
    new_lines = {l_id: LineCollection() for l_id in layers}

    line_arr = []
    line_arr_lines = []
    for l_id, lines in layers.items():
        line_arr.extend([[l_id, line] for line in lines.as_mls().geoms])
        line_arr_lines.extend([line for line in lines.as_mls().geoms])

    # Build R-tree from previous geometries
    tree = STRtree(line_arr_lines)
    index_by_id = dict((id(pt), i) for i, pt in enumerate(line_arr_lines))

    for i, (l_id, line) in enumerate(line_arr):
        coords = np.array(line.coords)

        if not (len(coords) > 3
                and math.hypot(coords[-1, 0] - coords[0, 0],
                               coords[-1, 1] - coords[0, 1]) < tolerance):
            continue

        p = Polygon(coords)
        geom_idx = [index_by_id[id(g)] for g in tree.query(p)]
        geom_idx = [idx for idx in geom_idx if idx < i]

        for gi in geom_idx:
            # Aggregate removed lines
            if keep_occulted:
                rl = p.intersection(line_arr_lines[gi])
                add_to_linecollection(removed_lines, rl)

            # Update previous geometries
            line_arr[gi][1] = line_arr[gi][1].difference(p)

    for (l_id, line) in line_arr:
        add_to_linecollection(new_lines[l_id], line)

    return new_lines, removed_lines
예제 #14
0
def test_find_nearest_within_reverse():
    lines = [[10, 0], [20, 10.1]]
    idx = LineIndex(LineCollection(lines))
    ridx = LineIndex(LineCollection(lines), reverse=True)

    assert idx.find_nearest_within(0.1, 0.5)[0] is None
    assert ridx.find_nearest_within(0.1, 0.5) == (0, True)
    assert ridx.find_nearest_within(10.01, 0.5) == (0, False)
    assert ridx.find_nearest_within(10.09, 0.5) == (1, True)
예제 #15
0
def crop(lines: vp.LineCollection, x: float, y: float, width: float, height: float):
    """Crop the geometries.

    The crop area is defined by the (X, Y) top-left corner and the WIDTH and HEIGHT arguments.
    All arguments understand supported units.
    """

    lines.crop(x, y, x + width, y + height)
    return lines
예제 #16
0
def reverse(line_collection: vp.LineCollection) -> vp.LineCollection:
    """Reverse order of lines.

    Reverse the order of lines within their respective layers. Individual lines are not
    modified (in particular, their trajectory is not inverted). Only the order in which they
    are drawn is reversed.
    """

    line_collection.reverse()
    return line_collection
예제 #17
0
파일: test_model.py 프로젝트: mfreyre/vpype
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
예제 #18
0
def test_document_lid_iteration():
    lc = LineCollection([(0, 1 + 1j)])
    doc = Document()
    doc.add(lc, 1)

    for lc in doc.layers_from_ids([1, 2, 3, 4]):
        lc.append([3, 3 + 3j])

    assert doc.count() == 1
    assert len(doc.layers[1]) == 2
예제 #19
0
def poly(coords):
    """Generate a single path with coordinates provided by the sequence of `--cords` option.

    Example:

        vpype poly coord -c 0 0 -c 1 0 -c 0 1 show
    """
    if len(coords) == 0:
        return LineCollection()
    else:
        return LineCollection([np.array([c[0] + 1j * c[1] for c in coords])])
예제 #20
0
def reloop(lines: vp.LineCollection, tolerance):
    """Randomize the seam location of closed paths.

    When plotted, closed path may exhibit a visible mark at the seam, i.e. the location where
    the pen begins and ends the stroke. This command randomizes the seam location in order to
    help reduce visual effect of this in plots with regular patterns.

    Paths are considered closed when their beginning and end points are closer than some
    tolerance, which can be set with the `--tolerance` option.
    """

    lines.reloop(tolerance=tolerance)
    return lines
예제 #21
0
def linemerge(lines: LineCollection, tolerance: float, no_flip: bool = True):
    """
    Merge lines whose endings overlap or are very close.

    Stroke direction is preserved by default, so `linemerge` looks at joining a line's end with
    another line's start. With the `--flip` stroke direction will be reversed as required to
    further the merge.

    By default, gaps of maximum 0.05mm are considered for merging. This can be controlled with
    the `--tolerance` option.
    """

    lines.merge(tolerance=tolerance, flip=not no_flip)
    return lines
예제 #22
0
def splitall(lines: LineCollection) -> LineCollection:
    """
    Split all paths into their constituent segments.

    This command may be used together with `linemerge` for cases such as densely-connected
    meshes where the latter cannot optimize well enough by itself.

    Note that since some paths (especially curved ones) can be made of a large number of
    segments, this command may significantly increase the processing time of the pipeline.
    """

    new_lines = LineCollection()
    for line in lines:
        new_lines.extend([line[i:i + 2] for i in range(len(line) - 1)])
    return new_lines
예제 #23
0
def linemerge(lines: vp.LineCollection, tolerance: float, no_flip: bool = True):
    """
    Merge lines whose endings and starts overlap or are very close.

    By default, `linemerge` considers both directions of a stroke. If there is no additional
    start of a stroke within the provided tolerance, it also checks for ending points of
    strokes and uses them in reverse. You can use the `--no-flip` to disable this reversing
    behaviour and preserve the stroke direction from the input.

    By default, gaps of maximum 0.05mm are considered for merging. This can be controlled with
    the `--tolerance` option.
    """

    lines.merge(tolerance=tolerance, flip=not no_flip)
    return lines
예제 #24
0
def circles(
    vector_data: Document,
    count,
    delta,
    quantization,
    layer_count,
    random_layer,
    layer,
    offset,
):

    start_layer_id = single_to_layer_id(layer, vector_data)
    for i in range(count):
        if random_layer:
            lid = start_layer_id + random.randint(0, layer_count - 1)
        else:
            lid = start_layer_id + (i % layer_count)

        vector_data.add(
            LineCollection([
                circle(
                    (i + 1) * delta, quantization) + offset[0] + 1j * offset[1]
            ]),
            lid,
        )

    return vector_data
예제 #25
0
파일: script.py 프로젝트: vmario89/vpype
def script(file) -> LineCollection:
    """
    Call an external python script to generate geometries.

    The script must contain a `generate()` function which will be called without arguments. It
    must return the generated geometries in one of the following format:

        - Shapely's MultiLineString
        - Iterable of Nx2 numpy float array
        - Iterable of Nx1 numpy complex array (where the real and imag part corresponds to
          the x, resp. y coordinates)

    All coordinates are expected to be in SVG pixel units (1/96th of an inch).
    """

    try:
        spec = importlib.util.spec_from_file_location("<external>", file)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)  # type: ignore
        return LineCollection(module.generate())  # type: ignore
    except Exception as exc:
        raise click.ClickException(
            (
                f"the file path must point to a Python script containing a `generate()`"
                f"function ({str(exc)})"
            )
        )
예제 #26
0
def line(x0: float, y0: float, x1: float, y1: float) -> LineCollection:
    """
    Generate a single line.

    The line starts at (X0, Y0) and ends at (X1, Y1). All arguments understand supported units.
    """
    return LineCollection([(complex(x0, y0), complex(x1, y1))])
예제 #27
0
def test_document_empty_copy():
    doc = Document()
    doc.add(LineCollection([(0, 1)]), 1)
    doc.page_size = 3, 4

    new_doc = doc.empty_copy()
    assert len(new_doc.layers) == 0
    assert new_doc.page_size == (3, 4)
예제 #28
0
def test_pop_front():
    lines = [random_line(5), random_line(3), random_line(7)]
    lc = LineCollection(lines)
    idx = LineIndex(lc)

    assert np.all(idx.pop_front() == lines[0])
    assert np.all(idx.pop_front() == lines[1])
    assert np.all(idx.pop_front() == lines[2])
    assert len(idx) == 0
예제 #29
0
def whlfarris(count) -> LineCollection:
    """Generates figure 2 of Farris' 1996 paper.

    Ref: https://core.ac.uk/download/pdf/72850999.pdf
    """
    return LineCollection([
        _wheelsonwheelsonwheels(count, [1, 7, -17], [1, 1 / 2, 1 / 3],
                                [0, 0, 0.25])
    ])
예제 #30
0
파일: frames.py 프로젝트: zxsq-cc/vpype
def frame(state: VpypeState, offset: float):
    """
    Add a single-line frame around the geometry.

    By default, the frame shape is the current geometries' bounding box. An optional offset can
    be provided.
    """
    if state.vector_data.is_empty():
        return LineCollection()

    bounds = state.vector_data.bounds() or (0, 0, 0, 0)
    return LineCollection([(
        bounds[0] - offset + 1j * (bounds[1] - offset),
        bounds[0] - offset + 1j * (bounds[3] + offset),
        bounds[2] + offset + 1j * (bounds[3] + offset),
        bounds[2] + offset + 1j * (bounds[1] - offset),
        bounds[0] - offset + 1j * (bounds[1] - offset),
    )])