summaryrefslogtreecommitdiff
path: root/lib/extensions/convert_to_satin.py
blob: 715853c48f4b233e8219c38624f70ae920c5ac98 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import inkex
from shapely import geometry as shgeo
from itertools import chain

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

    def generate_rungs(self, path, stroke_width):
        rungs = []

        # approximately 1cm between rungs, and we don't want them at the very start or end
        num_rungs = int(path.length / PIXELS_PER_MM / 10) - 1

        if num_rungs < 1 or num_rungs == 2:
            # avoid 2 rungs because it can be ambiguous (which are the rails and which are the
            # rungs?)
            num_rungs += 1

        distance_between_rungs = path.length / (num_rungs + 1)

        for i in xrange(num_rungs):
            rung_center = path.interpolate((i + 1) * distance_between_rungs)
            rung_center = Point(rung_center.x, rung_center.y)

            # TODO: use bezierslopeatt or whatever
            # we need to calculate the normal at this point so grab another point just a little
            # further down the path, effectively grabbing a small segment of the path
            segment_end = path.interpolate((i + 1) * distance_between_rungs + 0.1)
            segment_end = Point(segment_end.x, segment_end.y)

            tangent = segment_end - rung_center
            normal = tangent.unit().rotate_left()
            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
        import sys
        print >> sys.stderr, rails, 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",
            }
        )