inkstitch/lib/extensions/convert_to_satin.py

155 wiersze
5.5 KiB
Python
Czysty Zwykły widok Historia

2018-07-30 18:57:54 +00:00
import inkex
from shapely import geometry as shgeo
from itertools import chain
2018-07-30 20:03:27 +00:00
import numpy
from numpy import diff, sign, setdiff1d
from scipy.signal import argrelmin
import math
2018-07-30 18:57:54 +00:00
from .base import InkstitchExtension
from ..svg.tags import SVG_PATH_TAG
from ..svg import get_correction_transform, PIXELS_PER_MM
from ..elements import Stroke
from ..utils import Point
class ConvertToSatin(InkstitchExtension):
"""Convert a line to a satin column of the same width."""
def effect(self):
if not self.get_elements():
return
if not self.selected:
inkex.errormsg(_("Please select at least one line to convert to a satin column."))
return
if not all(isinstance(item, Stroke) for item in self.elements):
# L10N: Convert To Satin extension, user selected one or more objects that were not lines.
inkex.errormsg(_("Only simple lines may be converted to satin columns."))
return
for element in self.elements:
parent = element.node.getparent()
index = parent.index(element.node)
correction_transform = get_correction_transform(element.node)
for path in element.paths:
try:
rails, rungs = self.path_to_satin(path, element.stroke_width)
except ValueError:
inkex.errormsg(_("Cannot convert %s to a satin column because it intersects itself. Try breaking it up into multiple paths.") % element.node.get('id'))
parent.insert(index, self.satin_to_svg_node(rails, rungs, correction_transform))
parent.remove(element.node)
def path_to_satin(self, path, stroke_width):
path = shgeo.LineString(path)
left_rail = path.parallel_offset(stroke_width / 2.0, 'left')
right_rail = path.parallel_offset(stroke_width / 2.0, 'right')
if not isinstance(left_rail, shgeo.LineString) or \
not isinstance(right_rail, shgeo.LineString):
# If the parallel offsets come out as anything but a LineString, that means the
# path intersects itself, when taking its stroke width into consideration. See
# the last example for parallel_offset() in the Shapely documentation:
# https://shapely.readthedocs.io/en/latest/manual.html#object.parallel_offset
raise ValueError()
# for whatever reason, shapely returns a right-side offset's coordinates in reverse
left_rail = list(left_rail.coords)
right_rail = list(reversed(right_rail.coords))
rungs = self.generate_rungs(path, stroke_width)
return (left_rail, right_rail), rungs
2018-07-30 20:03:27 +00:00
def get_scores(self, path):
scores = numpy.zeros(101, numpy.int32)
path_length = path.length
prev_point = None
prev_direction = None
length_so_far = 0
for point in path.coords:
point = Point(*point)
if prev_point is None:
prev_point = point
continue
direction = (point - prev_point).unit()
if prev_direction is not None:
cos_angle_between = prev_direction * direction
angle = abs(math.degrees(math.acos(cos_angle_between)))
scores[int(round(length_so_far / path_length * 100.0))] += angle ** 2
2018-07-30 18:57:54 +00:00
2018-07-30 20:03:27 +00:00
length_so_far += (point - prev_point).length()
prev_direction = direction
prev_point = point
2018-07-30 18:57:54 +00:00
2018-07-30 20:03:27 +00:00
return scores
2018-07-30 18:57:54 +00:00
2018-07-30 20:03:27 +00:00
def local_minima(self, array):
# from: https://stackoverflow.com/a/9667121/4249120
# finds spots where the curvature (second derivative) is > 0
return (diff(sign(diff(array))) > 0).nonzero()[0] + 1
2018-07-30 18:57:54 +00:00
2018-07-30 20:03:27 +00:00
def generate_rungs(self, path, stroke_width):
scores = self.get_scores(path)
scores = numpy.convolve(scores, [1, 2, 4, 8, 16, 8, 4, 2, 1], mode='same')
rung_locations = self.local_minima(scores)
rung_locations = setdiff1d(rung_locations, [0, 100])
if len(rung_locations) == 0:
rung_locations = [50]
rungs = []
last_rung_center = None
for location in rung_locations:
location = location / 100.0
rung_center = path.interpolate(location, normalized=True)
2018-07-30 18:57:54 +00:00
rung_center = Point(rung_center.x, rung_center.y)
2018-07-30 20:03:27 +00:00
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
2018-07-30 18:57:54 +00:00
2018-07-30 20:03:27 +00:00
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()
2018-07-30 18:57:54 +00:00
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
def satin_to_svg_node(self, rails, rungs, correction_transform):
d = ""
for path in chain(rails, rungs):
d += "M"
for x, y in path:
d += "%s,%s " % (x, y)
d += " "
return inkex.etree.Element(SVG_PATH_TAG,
{
"id": self.uniqueId("path"),
"style": "stroke:#000000;stroke-width:1px;fill:none",
"transform": correction_transform,
"d": d,
"embroider_satin_column": "true",
}
)