summaryrefslogtreecommitdiff
path: root/lib/stitches/fill.py
diff options
context:
space:
mode:
authorLex Neva <github.com@lexneva.name>2018-05-01 20:37:51 -0400
committerLex Neva <github.com@lexneva.name>2018-05-01 20:37:51 -0400
commit1b31806423c8fec4040fed6d1009db016860b763 (patch)
tree71ccac169471e76685a7fa0b9910f64555dc73a7 /lib/stitches/fill.py
parent5b7f14d092456a941dbd189e61ed38d9b16d388b (diff)
rename inkstitch/ to lib/
You can't have a module and a package named the same thing. PyInstaller wants to import the main script as if it were a module, and this doesn't work unless there's no directory of the same name with a __init__.py in it.
Diffstat (limited to 'lib/stitches/fill.py')
-rw-r--r--lib/stitches/fill.py245
1 files changed, 245 insertions, 0 deletions
diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py
new file mode 100644
index 00000000..1b7377b0
--- /dev/null
+++ b/lib/stitches/fill.py
@@ -0,0 +1,245 @@
+from .. import PIXELS_PER_MM
+from ..utils import cache, Point as InkstitchPoint
+import shapely
+import math
+import sys
+
+
+def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers):
+ rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing, flip)
+ groups_of_segments = pull_runs(rows_of_segments, shape, row_spacing)
+
+ return [section_to_stitches(group, angle, row_spacing, max_stitch_length, staggers)
+ for group in groups_of_segments]
+
+
+@cache
+def east(angle):
+ # "east" is the name of the direction that is to the right along a row
+ return InkstitchPoint(1, 0).rotate(-angle)
+
+
+@cache
+def north(angle):
+ return east(angle).rotate(math.pi / 2)
+
+
+def row_num(point, angle, row_spacing):
+ return round((point * north(angle)) / row_spacing)
+
+
+def adjust_stagger(stitch, angle, row_spacing, max_stitch_length, staggers):
+ this_row_num = row_num(stitch, angle, row_spacing)
+ row_stagger = this_row_num % staggers
+ stagger_offset = (float(row_stagger) / staggers) * max_stitch_length
+ offset = ((stitch * east(angle)) - stagger_offset) % max_stitch_length
+
+ return stitch - offset * east(angle)
+
+def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers):
+ # We want our stitches to look like this:
+ #
+ # ---*-----------*-----------
+ # ------*-----------*--------
+ # ---------*-----------*-----
+ # ------------*-----------*--
+ # ---*-----------*-----------
+ #
+ # Each successive row of stitches will be staggered, with
+ # num_staggers rows before the pattern repeats. A value of
+ # 4 gives a nice fill while hiding the needle holes. The
+ # first row is offset 0%, the second 25%, the third 50%, and
+ # the fourth 75%.
+ #
+ # Actually, instead of just starting at an offset of 0, we
+ # can calculate a row's offset relative to the origin. This
+ # way if we have two abutting fill regions, they'll perfectly
+ # tile with each other. That's important because we often get
+ # abutting fill regions from pull_runs().
+
+ beg = InkstitchPoint(*beg)
+ end = InkstitchPoint(*end)
+
+ row_direction = (end - beg).unit()
+ segment_length = (end - beg).length()
+
+ # only stitch the first point if it's a reasonable distance away from the
+ # last stitch
+ if not stitches or (beg - stitches[-1]).length() > 0.5 * PIXELS_PER_MM:
+ stitches.append(beg)
+
+ first_stitch = adjust_stagger(beg, angle, row_spacing, max_stitch_length, staggers)
+
+ # we might have chosen our first stitch just outside this row, so move back in
+ if (first_stitch - beg) * row_direction < 0:
+ first_stitch += row_direction * max_stitch_length
+
+ offset = (first_stitch - beg).length()
+
+ while offset < segment_length:
+ stitches.append(beg + offset * row_direction)
+ offset += max_stitch_length
+
+ if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM:
+ stitches.append(end)
+
+
+def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=None, flip=False):
+ # the max line length I'll need to intersect the whole shape is the diagonal
+ (minx, miny, maxx, maxy) = shape.bounds
+ upper_left = InkstitchPoint(minx, miny)
+ lower_right = InkstitchPoint(maxx, maxy)
+ length = (upper_left - lower_right).length()
+ half_length = length / 2.0
+
+ # Now get a unit vector rotated to the requested angle. I use -angle
+ # because shapely rotates clockwise, but my geometry textbooks taught
+ # me to consider angles as counter-clockwise from the X axis.
+ direction = InkstitchPoint(1, 0).rotate(-angle)
+
+ # and get a normal vector
+ normal = direction.rotate(math.pi / 2)
+
+ # I'll start from the center, move in the normal direction some amount,
+ # and then walk left and right half_length in each direction to create
+ # a line segment in the grating.
+ center = InkstitchPoint((minx + maxx) / 2.0, (miny + maxy) / 2.0)
+
+ # I need to figure out how far I need to go along the normal to get to
+ # the edge of the shape. To do that, I'll rotate the bounding box
+ # angle degrees clockwise and ask for the new bounding box. The max
+ # and min y tell me how far to go.
+
+ _, start, _, end = shapely.affinity.rotate(shape, angle, origin='center', use_radians=True).bounds
+
+ # convert start and end to be relative to center (simplifies things later)
+ start -= center.y
+ end -= center.y
+
+ height = abs(end - start)
+
+ #print >> dbg, "grating:", start, end, height, row_spacing, end_row_spacing
+
+ # offset start slightly so that rows are always an even multiple of
+ # row_spacing_px from the origin. This makes it so that abutting
+ # fill regions at the same angle and spacing always line up nicely.
+ start -= (start + normal * center) % row_spacing
+
+ rows = []
+
+ current_row_y = start
+
+ while current_row_y < end:
+ p0 = center + normal * current_row_y + direction * half_length
+ p1 = center + normal * current_row_y - direction * half_length
+ endpoints = [p0.as_tuple(), p1.as_tuple()]
+ grating_line = shapely.geometry.LineString(endpoints)
+
+ res = grating_line.intersection(shape)
+
+ if (isinstance(res, shapely.geometry.MultiLineString)):
+ runs = map(lambda line_string: line_string.coords, res.geoms)
+ else:
+ if res.is_empty or len(res.coords) == 1:
+ # ignore if we intersected at a single point or no points
+ runs = []
+ else:
+ runs = [res.coords]
+
+ if runs:
+ runs.sort(key=lambda seg: (InkstitchPoint(*seg[0]) - upper_left).length())
+
+ if flip:
+ runs.reverse()
+ runs = map(lambda run: tuple(reversed(run)), runs)
+
+ rows.append(runs)
+
+ if end_row_spacing:
+ current_row_y += row_spacing + (end_row_spacing - row_spacing) * ((current_row_y - start) / height)
+ else:
+ current_row_y += row_spacing
+
+ return rows
+
+def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length, staggers):
+ stitches = []
+ first_segment = True
+ swap = False
+ last_end = None
+
+ for segment in group_of_segments:
+ (beg, end) = segment
+
+ if (swap):
+ (beg, end) = (end, beg)
+
+ stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers)
+
+ swap = not swap
+
+ return stitches
+
+
+def make_quadrilateral(segment1, segment2):
+ return shapely.geometry.Polygon((segment1[0], segment1[1], segment2[1], segment2[0], segment1[0]))
+
+
+def is_same_run(segment1, segment2, shape, row_spacing):
+ line1 = shapely.geometry.LineString(segment1)
+ line2 = shapely.geometry.LineString(segment2)
+
+ if line1.distance(line2) > row_spacing * 1.1:
+ return False
+
+ quad = make_quadrilateral(segment1, segment2)
+ quad_area = quad.area
+ intersection_area = shape.intersection(quad).area
+
+ return (intersection_area / quad_area) >= 0.9
+
+
+def pull_runs(rows, shape, row_spacing):
+ # Given a list of rows, each containing a set of line segments,
+ # break the area up into contiguous patches of line segments.
+ #
+ # This is done by repeatedly pulling off the first line segment in
+ # each row and calling that a shape. We have to be careful to make
+ # sure that the line segments are part of the same shape. Consider
+ # the letter "H", with an embroidery angle of 45 degrees. When
+ # we get to the bottom of the lower left leg, the next row will jump
+ # over to midway up the lower right leg. We want to stop there and
+ # start a new patch.
+
+ # for row in rows:
+ # print >> sys.stderr, len(row)
+
+ # print >>sys.stderr, "\n".join(str(len(row)) for row in rows)
+
+ runs = []
+ count = 0
+ while (len(rows) > 0):
+ run = []
+ prev = None
+
+ for row_num in xrange(len(rows)):
+ row = rows[row_num]
+ first, rest = row[0], row[1:]
+
+ # TODO: only accept actually adjacent rows here
+ if prev is not None and not is_same_run(prev, first, shape, row_spacing):
+ break
+
+ run.append(first)
+ prev = first
+
+ rows[row_num] = rest
+
+ # print >> sys.stderr, len(run)
+ runs.append(run)
+ rows = [row for row in rows if len(row) > 0]
+
+ count += 1
+
+ return runs
+