get rid of randomness in fills

Previously, each row in a fill region was offset by a random amount.  This
looks better than not offsetting at all (which will create visible columns of
stitches), but humans are very good at finding patterns in randomness.  The
result is that fill regions look weird.

This commit instead carefully orders the stitches in fill regions such that
each row's stitches are offset by half of max_stitch_length from the previous.

There are also a couple of other miscellaneous fixes and features in here --
sorry about that.
pull/1/head
Lex Neva 2016-02-18 21:40:37 -05:00
rodzic cf81db3670
commit d1cd63eecd
3 zmienionych plików z 190 dodań i 66 usunięć

Wyświetl plik

@ -382,7 +382,9 @@ class PatchList:
if p is not point and p.patch == point.patch:
return p
for starting_point in self.pointList:
start_time = time.time()
for starting_point in random.sample(self.pointList, len(self.pointList)):
point_list = self.pointList[:]
last_point = mate(starting_point)
@ -406,6 +408,10 @@ class PatchList:
min_cost = cost
min_path = path
# timebox this bit to avoid spinning our wheels forever
if time.time() - start_time > 1.0:
break
for point in min_path:
takePatchStartingAtPoint(point)
@ -569,6 +575,9 @@ class SortOrder:
#dbg.write("preserve_layers is false:\n");
self.sorttuple = (threadcolor,)
def append(self, criterion):
self.sorttuple += (criterion,)
def __cmp__(self, other):
return cmp(self.sorttuple, other.sorttuple)
@ -648,39 +657,120 @@ class Embroider(inkex.Effect):
max_stitch_len_px = get_float_param(node, "max_stitch_length", self.max_stitch_len_px)
rows_of_segments = self.intersect_region_with_grating(shpath, row_spacing_px, angle)
segments = self.visit_segments_one_by_one(rows_of_segments, shpath)
groups_of_segments = self.pull_runs(rows_of_segments, shpath, row_spacing_px)
#print >> sys.stderr, len(groups_of_segments)
patches = []
for group_of_segments in groups_of_segments:
patch = Patch(color=threadcolor,sortorder=sortorder)
first_segment = True
swap = False
last_length = 0
last_end = None
for segment in group_of_segments:
# want to try to keep needle-holes as far apart as possible
# (tatami fill)
# check last stitch in last row. If less than half of max stitch length,
# go straight for the midpoint between the last two stitches in the last
# row. Otherwise, go for the midpoint between the last stitch and the end
# of the last row, then go for the midpoint between the last two stitches
(beg, end) = segment
if (swap):
(beg,end)=(end,beg)
if not hatching:
swap = not swap
beg = PyEmb.Point(*beg)
end = PyEmb.Point(*end)
along = (end - beg).unit()
segment_length = (end - beg).length()
# We want to try to interleave stitches so that the needle-
# holes are as far apart as possible from the previous row's
# holes. It should look like this:
#
# ---1---2---3---4-|
# -8---7---6---5---|
#
# This represents filling a region with two successive rows
# of stitching, where the numbers indicate the positions that
# the needle punctures the fabric and engages the bobbin
# thread. We need to make sure that stitch 5 falls between
# 3 and 4.
#
# The | characters indicate the right side of the fill area.
# The trick is that the space after stitch 4 is entirely
# dependent on how much space is left after filling the row
# with stitches of length max_stitch_len_px (M). Call that
# remainder R. It could be anywhere from 0 to M.
#
# There's one more wrinkle. In reality, if the side of the
# fill region is not perpendicular, we have this:
#
# ---1---2---3---4--/
# -8---7---6---5---/
#
# In this example, R is shortened by the angled side. We can
# figure out how much it's shortened by taking the dot product
# of the side segment and a unit vector in the direction of the
# second row, to get the projection, P:
#
# ---1---2---3---4-P/
# -8---7---6---5---/
#
# Therefore, stitch 5 is at position D = R - P + M/2. It's
# possible that D may be greater than M, so we need to stitch
# one or more times before D. It's also possible, in extreme
# fill shapes, that D is negative. In that case we should
# add successive Ms until we get back into the fill region.
#
# Geometry is hard.
d = last_length
if first_segment:
first_segment = False
else:
side = beg - last_end
d -= side * along
d += max_stitch_len_px / 2.0
while d < 0:
d += max_stitch_len_px
def small_stitches(patch, beg, end):
vector = (end-beg)
patch.addStitch(beg)
old_dist = vector.length()
if (old_dist < max_stitch_len_px):
patch.addStitch(end)
return
one_stitch = vector.mul(1.0 / old_dist * max_stitch_len_px * random.random())
beg = beg + one_stitch
while (True):
vector = (end-beg)
dist = vector.length()
assert(old_dist==None or dist<old_dist)
old_dist = dist
patch.addStitch(beg)
if (dist < max_stitch_len_px):
runup = []
while d > 0:
if d < segment_length:
runup.insert(0, beg + along * d)
d -= max_stitch_len_px
for stitch in runup:
patch.addStitch(stitch)
# Now we'll set d as the distance of the last stitch along the
# segment and continue on.
d = (patch.stitches[-1] - beg) * along
d += max_stitch_len_px
while d < segment_length:
patch.addStitch(beg + d * along)
d += max_stitch_len_px
# add the endpoint if we're not already there
last_length = (end - patch.stitches[-1]).length()
last_end = end
if last_length > 0.1 * pixels_per_millimeter:
# skip the stitch if it's ridiculously short
patch.addStitch(end)
return
one_stitch = vector.mul(1.0/dist*max_stitch_len_px)
beg = beg + one_stitch
swap = False
patch = Patch(color=threadcolor,sortorder=sortorder)
for (beg,end) in segments:
if (swap):
(beg,end)=(end,beg)
if not hatching:
swap = not swap
small_stitches(patch, PyEmb.Point(*beg),PyEmb.Point(*end))
return [patch]
patches.append(patch)
return patches
def intersect_region_with_grating(self, shpath, row_spacing_px, angle):
# the max line length I'll need to intersect the whole shape is the diagonal
@ -735,9 +825,10 @@ class Embroider(inkex.Effect):
rows.append(runs)
start += row_spacing_px
return rows
def visit_segments_one_by_one(self, rows, shpath):
def pull_runs(self, rows, shpath, row_spacing_px):
# Given a list of rows, each containing a set of line segments,
# break the area up into contiguous patches of line segments.
#
@ -749,46 +840,57 @@ class Embroider(inkex.Effect):
# over to midway up the lower right leg. We want to stop there and
# start a new patch.
# Segments more than this far apart are considered not to be part of
# the same run.
row_distance_cutoff = row_spacing_px * 1.1
def make_quadrilateral(segment1, segment2):
return shgeo.Polygon((segment1[0], segment1[1], segment2[1], segment2[0], segment1[0]))
def pull_runs(rows):
new_rows = []
run = []
prev = None
done = False
for r in rows:
if done:
new_rows.append(r)
continue
def is_same_run(segment1, segment2):
if self.options.hatch_filled_paths:
return True
(first,rest) = (r[0], r[1:])
if shgeo.LineString(segment1).distance(shgeo.LineString(segment1)) > row_spacing_px * 1.1:
return False
if prev is not None:
quad = make_quadrilateral(prev, first)
quad_area = quad.area
intersection_area = shpath.intersection(quad).area
quad = make_quadrilateral(segment1, segment2)
quad_area = quad.area
intersection_area = shpath.intersection(quad).area
if intersection_area / quad_area < .9:
new_rows.append(r)
done = True
continue
return (intersection_area / quad_area) >= 0.9
run.append(first)
prev = first
if (len(rest)>0):
new_rows.append(rest)
return (run, new_rows)
#for row in rows:
# print >> sys.stderr, len(row)
linearized_runs = []
#print >>sys.stderr, "\n".join(str(len(row)) for row in rows)
runs = []
count = 0
while (len(rows) > 0):
(one_run,rows) = pull_runs(rows)
linearized_runs.extend(one_run)
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):
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]
rows = rows[::-1]
count += 1
return linearized_runs
return runs
def handle_node(self, node):
if (node.tag == inkex.addNS('g', 'svg')):
@ -803,10 +905,23 @@ class Embroider(inkex.Effect):
if get_boolean_param(node, "satin_column"):
self.patchList.patches.extend(self.satin_column(node))
else:
if (self.get_style(node, "fill")!=None):
self.patchList.patches.extend(self.filled_region_to_patchlist(node))
stroke = []
fill = []
if (self.get_style(node, "stroke")!=None):
self.patchList.patches.extend(self.path_to_patch_list(node))
stroke = self.path_to_patch_list(node)
if (self.get_style(node, "fill")!=None):
fill = self.filled_region_to_patchlist(node)
if get_boolean_param(node, "stroke_first", False):
for patch in stroke:
patch.sortorder.append(0)
for patch in fill:
patch.sortorder.append(1)
self.patchList.patches.extend(stroke)
self.patchList.patches.extend(fill)
def get_style(self, node, style_name):
style = simplestyle.parseStyle(node.get("style"))
@ -854,14 +969,21 @@ class Embroider(inkex.Effect):
self.svgpath = inkex.addNS('path', 'svg')
self.patchList = PatchList([])
dbg.write("starting nodes: %s" % time.time())
dbg.flush()
for node in self.selected.itervalues():
self.handle_node(node)
dbg.write("finished nodes: %s" % time.time())
dbg.flush()
if not self.patchList:
inkex.errormsg("No paths selected.")
return
dbg.write("starting tsp: %s" % time.time())
self.patchList = self.patchList.tsp_by_color()
dbg.write("finished tsp: %s" % time.time())
#dbg.write("patch count: %d\n" % len(self.patchList.patches))
if self.options.hide_layers:
@ -1013,7 +1135,7 @@ class Embroider(inkex.Effect):
id = node.get("id")
# First, verify that we have a valid node.
csp = cubicsuperpath.parsePath(node.get("d"))
csp = parse_path(node)
self.validate_satin_column(node, csp)
# fetch parameters

Wyświetl plik

@ -5,13 +5,14 @@
<dependency type="executable" location="extensions">embroider_params.py</dependency>
<dependency type="executable" location="extensions">inkex.py</dependency>
<param name="zigzag_spacing" type="string" _gui-text="Zigzag spacing (pixels)"></param>
<param name="running_stitch_length" type="string" _gui-text="Running stitch length (pixels)"></param>
<param name="stitch_length" type="string" _gui-text="Running stitch length (pixels)"></param>
<param name="row_spacing" type="string" _gui-text="Row spacing (pixels)"></param>
<param name="max_stitch_length" type="string" _gui-text="Maximum stitch length (pixels)"></param>
<param name="repeats" type="string" _gui-text="Repeats (stroke only)"></param>
<param name="angle" type="string" _gui-text="Angle for lines in fills"></param>
<param name="hatching" type="string" _gui-text="Hatching? (yes/no)"></param>
<param name="satin_column" type="string" _gui-text="Satin Column? (yes/no)"></param>
<param name="stroke_first" type="string" _gui-text="Stitch stroke before fill? (yes/no)"></param>
<effect>
<object-type>all</object-type>
<effects-menu>

Wyświetl plik

@ -16,13 +16,14 @@ class EmbroiderParams(inkex.Effect):
inkex.Effect.__init__(self)
self.params = ["zigzag_spacing",
"running_stitch_length",
"stitch_length",
"row_spacing",
"max_stitch_length",
"repeats",
"angle",
"hatching",
"satin_column"
"satin_column",
"stroke_first",
]
for param in self.params: