summaryrefslogtreecommitdiff
path: root/lib/extensions/convert_to_satin.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/extensions/convert_to_satin.py')
-rw-r--r--lib/extensions/convert_to_satin.py82
1 files changed, 80 insertions, 2 deletions
diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py
index 8af45b3b..53930935 100644
--- a/lib/extensions/convert_to_satin.py
+++ b/lib/extensions/convert_to_satin.py
@@ -67,6 +67,17 @@ class ConvertToSatin(InkstitchExtension):
return (left_rail, right_rail), rungs
def get_scores(self, path):
+ """Generate an array of "scores" of the sharpness of corners in a path
+
+ A higher score means that there are sharper corners in that section of
+ the path. We'll divide the path into boxes, with the score in each
+ box indicating the sharpness of corners at around that percentage of
+ the way through the path. For example, if scores[40] is 100 and
+ scores[45] is 200, then the path has sharper corners at a spot 45%
+ along its length than at a spot 40% along its length.
+ """
+
+ # need 101 boxes in order to encompass percentages from 0% to 100%
scores = numpy.zeros(101, numpy.int32)
path_length = path.length
@@ -83,9 +94,19 @@ class ConvertToSatin(InkstitchExtension):
direction = (point - prev_point).unit()
if prev_direction is not None:
+ # The dot product of two vectors is |v1| * |v2| * cos(angle).
+ # These are unit vectors, so their magnitudes are 1.
cos_angle_between = prev_direction * direction
angle = abs(math.degrees(math.acos(cos_angle_between)))
+ # Use the square of the angle, measured in degrees.
+ #
+ # Why the square? This penalizes bigger angles more than
+ # smaller ones.
+ #
+ # Why degrees? This is kind of arbitrary but allows us to
+ # use integer math effectively and avoid taking the square
+ # of a fraction between 0 and 1.
scores[int(round(length_so_far / path_length * 100.0))] += angle ** 2
length_so_far += (point - prev_point).length()
@@ -96,40 +117,97 @@ class ConvertToSatin(InkstitchExtension):
def local_minima(self, array):
# from: https://stackoverflow.com/a/9667121/4249120
- # finds spots where the curvature (second derivative) is > 0
+ # This finds spots where the curvature (second derivative) is > 0.
+ #
+ # This method has the convenient benefit of choosing points around
+ # 5% before and after a sharp corner such as in a square.
return (diff(sign(diff(array))) > 0).nonzero()[0] + 1
def generate_rungs(self, path, stroke_width):
+ """Create rungs for a satin column.
+
+ Where should we put the rungs along a path? We want to ensure that the
+ resulting satin matches the original path as closely as possible. We
+ want to avoid having a ton of rungs that will annoy the user. We want
+ to ensure that the rungs we choose actually intersect both rails.
+
+ We'll place a few rungs perpendicular to the tangent of the path.
+ Things get pretty tricky at sharp corners. If we naively place a rung
+ perpendicular to the path just on either side of a sharp corner, the
+ rung may not intersect both paths:
+ | |
+ _______________| |
+ ______|_
+ ____________________|
+
+ It'd be best to place rungs in the straight sections before and after
+ the sharp corner and allow the satin column to bend the stitches around
+ the corner automatically.
+
+ How can we find those spots?
+
+ The general algorithm below is:
+
+ * assign a "score" to each section of the path based on how sharp its
+ corners are (higher means a sharper corner)
+ * pick spots with lower scores
+ """
+
scores = self.get_scores(path)
+
+ # This is kind of like a 1-dimensional gaussian blur filter. We want to
+ # avoid the area near a sharp corner, so we spread out its effect for
+ # 5 buckets in either direction.
scores = numpy.convolve(scores, [1, 2, 4, 8, 16, 8, 4, 2, 1], mode='same')
+
+ # Now we'll find the spots that aren't near corners, whose scores are
+ # low -- the local minima.
rung_locations = self.local_minima(scores)
+
+ # Remove the start and end, because we can't stick a rung there.
rung_locations = setdiff1d(rung_locations, [0, 100])
if len(rung_locations) == 0:
+ # Straight lines won't have local minima, so add a rung in the center.
rung_locations = [50]
rungs = []
last_rung_center = None
for location in rung_locations:
+ # Convert percentage to a fraction so that we can use interpolate's
+ # normalized parameter.
location = location / 100.0
+
rung_center = path.interpolate(location, normalized=True)
rung_center = Point(rung_center.x, rung_center.y)
+ # Avoid placing rungs too close together. This somewhat
+ # arbitrarily rejects the rung if there was one less than 2
+ # millimeters before this one.
if last_rung_center is not None and \
(rung_center - last_rung_center).length() < 2 * PIXELS_PER_MM:
continue
else:
last_rung_center = rung_center
+ # We need to know the tangent of the path's curve at this point.
+ # Pick another point just after this one and subtract them to
+ # approximate a tangent vector.
tangent_end = path.interpolate(location + 0.001, normalized=True)
tangent_end = Point(tangent_end.x, tangent_end.y)
tangent = (tangent_end - rung_center).unit()
+
+ # Rotate 90 degrees left to make a normal vector.
normal = tangent.rotate_left()
- offset = normal * stroke_width * 0.75
+ # Travel 75% of the stroke width left and right to make the rung's
+ # endpoints. This means the rung's length is 150% of the stroke
+ # width.
+ offset = normal * stroke_width * 0.75
rung_start = rung_center + offset
rung_end = rung_center - offset
+
rungs.append((rung_start.as_tuple(), rung_end.as_tuple()))
return rungs