summaryrefslogtreecommitdiff
path: root/lib/stitches/circular_fill.py
blob: 959759dc91306be507dc29f46eedd1069a2a529a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
from shapely import geometry as shgeo
from shapely.ops import substring

from ..stitch_plan import Stitch
from ..utils.geometry import reverse_line_string
from .auto_fill import (build_fill_stitch_graph, build_travel_graph,
                        collapse_sequential_outline_edges, fallback,
                        find_stitch_path, graph_is_valid, travel)
from .contour_fill import _make_fermat_spiral
from .running_stitch import bean_stitch, running_stitch


def circular_fill(shape,
                  angle,
                  row_spacing,
                  end_row_spacing,
                  num_staggers,
                  running_stitch_length,
                  running_stitch_tolerance,
                  bean_stitch_repeats,
                  repeats,
                  skip_last,
                  starting_point,
                  ending_point,
                  underpath,
                  target
                  ):

    # get furthest distance of the target point to a shape border
    # so we know how many circles we will need
    distance = shape.hausdorff_distance(target)
    radius = row_spacing
    center = shgeo.Point(target)

    if radius > distance:
        # if the shape is smaller than row_spacing, return a simple circle in the size of row_spacing
        stitches = running_stitch([Stitch(*point) for point in center.buffer(radius).exterior.coords],
                                  running_stitch_length, running_stitch_tolerance)
        return _apply_bean_stitch_and_repeats(stitches, repeats, bean_stitch_repeats)

    circles = []
    # add a small inner circle to make sure that the spiral ends close to the center
    circles.append(shgeo.LineString(center.buffer(0.1).exterior.coords))
    # add twice the size of the (end_)row_spacing to make sure we go big enough
    stopp_at_distance = distance + (end_row_spacing or row_spacing) * 2
    while radius < stopp_at_distance:
        circles.append(shgeo.LineString(center.buffer(radius).exterior.coords))
        if end_row_spacing:
            radius += row_spacing + (end_row_spacing - row_spacing) * (radius / distance)
        else:
            radius += row_spacing
    circles.reverse()

    # Use double spiral from contour fill (we don't want to get stuck in the middle of the spiral)
    double_spiral = _make_fermat_spiral(circles, running_stitch_length, circles[0].coords[0])
    double_spiral = shgeo.LineString(list(double_spiral))
    intersection = double_spiral.intersection(shape)

    if isinstance(intersection, shgeo.LineString):
        # if we get a single linestrig (original shape is a circle), apply start and end commands and return path
        path = list(intersection.coords)
        path = _apply_start_end_commands(shape, path, starting_point, ending_point)
        stitches = running_stitch([Stitch(*point) for point in path], running_stitch_length, running_stitch_tolerance)
        return _apply_bean_stitch_and_repeats(stitches, repeats, bean_stitch_repeats)

    segments = []
    for line in intersection.geoms:
        if isinstance(line, shgeo.LineString):
            # use running stitch here to adjust the stitch length
            coords = running_stitch([Stitch(point[0], point[1]) for point in line.coords],
                                    running_stitch_length,
                                    running_stitch_tolerance)
            segments.append([(point.x, point.y) for point in coords])

    fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
    if not graph_is_valid(fill_stitch_graph):
        return fallback(shape, running_stitch_length, running_stitch_tolerance)

    travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
    path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
    result = path_to_stitches(shape, path, travel_graph, fill_stitch_graph, running_stitch_length, running_stitch_tolerance, skip_last, underpath)
    result = _apply_bean_stitch_and_repeats(result, repeats, bean_stitch_repeats)
    return result


def _apply_bean_stitch_and_repeats(stitches, repeats, bean_stitch_repeats):
    if any(bean_stitch_repeats):
        # add bean stitches, but ignore travel stitches
        stitches = bean_stitch(stitches, bean_stitch_repeats, ['auto_fill_travel'])

    if repeats:
        for i in range(1, repeats):
            if i % 2 == 1:
                # reverse every other pass
                stitches.extend(stitches[::-1])
            else:
                stitches.extend(stitches)

    return stitches


def _apply_start_end_commands(shape, path, starting_point, ending_point):
    if starting_point or ending_point:
        outline = shape.boundary
        if starting_point:
            start = _get_start_end_sequence(outline, shgeo.Point(*starting_point), shgeo.Point(*path[0]))
            path = list(start.coords) + path
        if ending_point:
            end = _get_start_end_sequence(outline, shgeo.Point(*path[-1]), shgeo.Point(*ending_point))
            path.extend(list(end.coords))
    return path


def _get_start_end_sequence(outline, start, end):
    start_dist = outline.project(start)
    end_dist = outline.project(end)
    return substring(outline, start_dist, end_dist)


def path_to_stitches(shape, path, travel_graph, fill_stitch_graph, running_stitch_length, running_stitch_tolerance, skip_last, underpath):
    path = collapse_sequential_outline_edges(path, fill_stitch_graph)

    stitches = []

    # If the very first stitch is travel, we'll omit it in travel(), so add it here.
    if not path[0].is_segment():
        stitches.append(Stitch(*path[0].nodes[0], tags={'auto_fill_travel'}))

    for edge in path:
        if edge.is_segment():
            current_edge = fill_stitch_graph[edge[0]][edge[-1]]['segment']
            path_geometry = current_edge['geometry']

            if edge[0] != path_geometry.coords[0]:
                path_geometry = reverse_line_string(path_geometry)

            new_stitches = [Stitch(*point) for point in path_geometry.coords]

            # need to tag stitches
            if skip_last:
                del new_stitches[-1]

            stitches.extend(new_stitches)

            travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', []))
        else:
            stitches.extend(travel(shape, travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last, underpath))

    return stitches