summaryrefslogtreecommitdiff
path: root/lib/extensions/knockdown_fill.py
blob: e8ac7d3ae510a4f5a0fa26c96e211a431d57fa1a (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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# Authors: see git history
#
# Copyright (c) 2025 Authors
# Licensed under the GNU GPL version 3.0 or later.  See the file LICENSE for details.

from inkex import Boolean, Path, PathElement
from shapely import minimum_bounding_circle, union_all
from shapely.geometry import LineString, Polygon

from ..stitches.ripple_stitch import ripple_stitch
from ..svg import PIXELS_PER_MM, get_correction_transform
from ..utils.geometry import ensure_multi_polygon
from .base import InkstitchExtension


class KnockdownFill(InkstitchExtension):
    '''
    This extension generates a shape around all selected shapes and inserts it into the document
    '''
    def __init__(self, *args, **kwargs):
        InkstitchExtension.__init__(self, *args, **kwargs)
        self.arg_parser.add_argument("--notebook")
        self.arg_parser.add_argument("-k", "--keep-holes", type=Boolean, default=True, dest="keep_holes")
        self.arg_parser.add_argument("-o", "--offset", type=float, default=0, dest="offset")
        self.arg_parser.add_argument("-j", "--join-style", type=str, default="1", dest="join_style")
        self.arg_parser.add_argument("-m", "--mitre-limit", type=float, default=5.0, dest="mitre_limit")

        self.arg_parser.add_argument("-s", "--shape", type=str, default='', dest="shape")
        self.arg_parser.add_argument("-f", "--shape-offset", type=float, default=0, dest="shape_offset")
        self.arg_parser.add_argument("-p", "--shape-join-style", type=str, default="1", dest="shape_join_style")
        # TODO: Layer options: underlay, row spacing, angle

    def effect(self):
        if not self.get_elements():
            return

        polygons = []
        for element in self.elements:
            polygons.extend(self.element_outlines(element))
        combined_shape = union_all(polygons)

        offset_shape = self._apply_offset(combined_shape, self.options.offset, self.options.join_style)
        offset_shape = offset_shape.simplify(0.3)
        offset_shape = ensure_multi_polygon(offset_shape)

        self.insert_knockdown_elements(offset_shape)

    def element_outlines(self, element):
        polygons = []
        if element.name == "FillStitch":
            # take expand value into account
            shape = element.shrink_or_grow_shape(element.shape, element.expand)
            # MultiPolygon
            for polygon in shape.geoms:
                polygons.append(polygon)
        elif element.name == "SatinColumn":
            # plot points on rails, so we get the actual satin size (including pull compensation)
            rail_pairs = zip(*element.plot_points_on_rails(
                0.3,
                element.pull_compensation_px,
                element.pull_compensation_percent / 100)
            )
            rails = []
            for rail in rail_pairs:
                rails.append(LineString(rail))
            polygon = Polygon(list(rails[0].coords) + list(rails[1].reverse().coords)).buffer(0)
            polygons.append(polygon)
        elif element.name == "Stroke":
            if element.stroke_method == 'ripple_stitch':
                # for ripples this is going to be a bit complicated, so let's follow the stitch plan
                stitches = ripple_stitch(element)
                linestring = LineString(stitches)
                polygons.append(linestring.buffer(0.15 * PIXELS_PER_MM, cap_style='flat'))
            elif element.stroke_method == 'zigzag_stitch':
                # zigzag stitch depends on the width of the stroke and pull compensation settings
                polygons.append(element.as_multi_line_string().buffer((element.stroke_width + element.pull_compensation) / 2, cap_style='flat'))
            else:
                polygons.append(element.as_multi_line_string().buffer(0.15 * PIXELS_PER_MM, cap_style='flat'))
        elif element.name == "Clone":
            with element.clone_elements() as elements:
                for clone_child in elements:
                    polygons.extend(self.element_outlines(clone_child))
        return polygons

    def _apply_offset(self, shape, offset_mm, join_style):
        return shape.buffer(
            offset_mm * PIXELS_PER_MM,
            cap_style=int(join_style),
            join_style=int(join_style),
            mitre_limit=float(max(self.options.mitre_limit, 0.1))
        )

    def insert_knockdown_elements(self, combined_shape):
        first = self.svg.selection.rendering_order()[0]
        try:
            parent = first.getparent()
            index = parent.index(first)
        except AttributeError:
            parent = self.svg
            index = 0
        transform = get_correction_transform(first)

        for polygon in combined_shape.geoms:
            d = str(Path(polygon.exterior.coords))
            if self.options.keep_holes:
                for interior in polygon.interiors:
                    d += str(Path(interior.coords))

            if self.options.shape == 'rect':
                rect = polygon.envelope
                offset_rect = self._apply_offset(rect, self.options.shape_offset, self.options.shape_join_style)
                offset_rect = offset_rect.reverse()
                d = str(Path(offset_rect.exterior.coords)) + d
            elif self.options.shape == 'circle':
                circle = minimum_bounding_circle(polygon)
                offset_circle = self._apply_offset(circle, self.options.shape_offset, self.options.shape_join_style)
                offset_circle = offset_circle.reverse()
                d = str(Path(offset_circle.exterior.coords)) + d

            path = PathElement()
            path.set('d', d)
            path.label = self.svg.get_unique_id('Knockdown ')
            path.set('transform', transform)

            path.set('inkstitch:row_spacing_mm', '2.6')
            path.set('inkstitch:fill_underlay_angle', '60 -60')
            path.set('inkstitch:fill_underlay_max_stitch_length_mm', '3')
            path.set('inkstitch:fill_underlay_row_spacing_mm', '2.6')
            path.set('inkstitch:underlay_underpath', 'False')
            path.set('inkstitch:underpath', 'False')
            path.set('inkstitch:staggers', '2')
            path.set('style', 'fill:black;')

            parent.insert(index, path)