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)
|