inkstitch/lib/extensions/knockdown_fill.py

155 wiersze
6.7 KiB
Python

# 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)
if self.options.shape:
self._insert_embossed_path(combined_shape, transform, parent, index)
else:
self._insert_knockdown_path(combined_shape, transform, parent, index)
def _insert_embossed_path(self, combined_shape, transform, parent, index):
if self.options.shape == 'rect':
rect = combined_shape.envelope
offset_shape = self._apply_offset(rect, self.options.shape_offset, self.options.shape_join_style)
else:
circle = minimum_bounding_circle(combined_shape)
offset_shape = self._apply_offset(circle, self.options.shape_offset, self.options.shape_join_style)
offset_shape = offset_shape.reverse()
d = str(Path(offset_shape.exterior.coords))
for polygon in combined_shape.geoms:
d += str(Path(polygon.exterior.coords))
d += self._get_hole_paths(polygon)
self.insert_path(d, transform, parent, index)
def _insert_knockdown_path(self, combined_shape, transform, parent, index):
for polygon in combined_shape.geoms:
d = str(Path(polygon.exterior.coords))
d += self._get_hole_paths(polygon)
self.insert_path(d, transform, parent, index)
def _get_hole_paths(self, polygon):
d = ''
if not self.options.keep_holes:
return d
for interior in polygon.interiors:
d += str(Path(interior.coords))
return d
def insert_path(self, d, transform, parent, index):
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)