コード例 #1
0
ファイル: operations.py プロジェクト: tatarize/vpype
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
ファイル: test_model.py プロジェクト: vmario89/vpype
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
ファイル: test_model.py プロジェクト: vmario89/vpype
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
ファイル: test_model.py プロジェクト: vmario89/vpype
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
ファイル: operations.py プロジェクト: tatarize/vpype
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
ファイル: test_model.py プロジェクト: vmario89/vpype
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
ファイル: test_line_index.py プロジェクト: timClicks/vpype
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
ファイル: operations.py プロジェクト: vmario89/vpype
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
ファイル: operations.py プロジェクト: tatarize/vpype
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
ファイル: test_model.py プロジェクト: vmario89/vpype
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
ファイル: operations.py プロジェクト: tatarize/vpype
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
ファイル: operations.py プロジェクト: vmario89/vpype
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
ファイル: test_model.py プロジェクト: vmario89/vpype
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
ファイル: test_line_index.py プロジェクト: timClicks/vpype
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),
    )])