kopia lustrzana https://github.com/inkstitch/inkstitch
add documentation
rodzic
5f14617a02
commit
8bf478a71a
|
@ -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()
|
||||
normal = tangent.rotate_left()
|
||||
offset = normal * stroke_width * 0.75
|
||||
|
||||
# Rotate 90 degrees left to make a normal vector.
|
||||
normal = tangent.rotate_left()
|
||||
|
||||
# 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
|
||||
|
|
Ładowanie…
Reference in New Issue