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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from copy import copy
from typing import List, Tuple, Union
from inkex import Path, errormsg
from shapely.geometry import LinearRing, MultiPolygon, Polygon
from shapely.ops import polygonize, unary_union
from ..elements import EmbroideryElement
from ..i18n import _
from ..svg import get_correction_transform
from ..svg.tags import SVG_PATH_TAG
from .base import InkstitchExtension
class BreakApart(InkstitchExtension):
'''
This will break apart fill areas into separate elements.
'''
def __init__(self, *args, **kwargs):
InkstitchExtension.__init__(self, *args, **kwargs)
self.arg_parser.add_argument("--notebook")
self.arg_parser.add_argument("-m", "--method", type=int, default=1, dest="method")
self.minimum_size = 5
def effect(self) -> None: # noqa: C901
if not self.svg.selection:
errormsg(_("Please select one or more fill areas to break apart."))
return
elements = []
nodes = self.get_nodes()
for node in nodes:
if node.tag in SVG_PATH_TAG:
elements.append(EmbroideryElement(node))
for element in elements:
if not element.get_style("fill", "black"):
continue
# we don't want to touch valid elements
paths = element.flatten(element.parse_path())
try:
paths.sort(key=lambda point_list: Polygon(point_list).area, reverse=True)
polygon = MultiPolygon([(paths[0], paths[1:])])
if self.geom_is_valid(polygon) and Polygon(paths[-1]).area > self.minimum_size:
continue
except ValueError:
pass
polygons = self.break_apart_paths(paths)
if self.options.method == 1:
polygons = self.combine_overlapping_polygons(polygons)
polygons = self.recombine_polygons(polygons)
if polygons:
self.polygons_to_nodes(polygons, element)
def break_apart_paths(self, paths: List[List[Union[List[float], Tuple[float, float]]]]) -> List[Polygon]:
polygons = []
for path in paths:
if len(path) < 3:
continue
linearring = LinearRing(path)
if not linearring.is_simple:
union = unary_union(linearring)
for polygon in polygonize(union):
polygons.append(polygon)
else:
polygon = Polygon(path).buffer(0)
polygons.append(polygon)
return polygons
def combine_overlapping_polygons(self, polygons: List[Polygon]) -> List[Polygon]:
for polygon in polygons:
for other in polygons:
if polygon == other:
continue
if polygon.overlaps(other):
diff = polygon.symmetric_difference(other)
if isinstance(diff, MultiPolygon):
polygons.remove(other)
polygons.remove(polygon)
for p in diff.geoms:
polygons.append(p)
# it is possible, that a polygons overlap with multiple
# polygons, this means, we need to start all over again
polygons = self.combine_overlapping_polygons(polygons)
return polygons
return polygons
def geom_is_valid(self, geom: MultiPolygon) -> bool:
valid = geom.is_valid
return valid
def ensure_minimum_size(self, polygons: List[Polygon], size: int) -> List[Polygon]:
for polygon in polygons:
if polygon.area < size:
polygons.remove(polygon)
return polygons
def recombine_polygons(self, polygons: List[Polygon]) -> List[List[Polygon]]:
polygons.sort(key=lambda polygon: polygon.area, reverse=True)
multipolygons = []
holes = []
self.ensure_minimum_size(polygons, self.minimum_size)
for polygon in polygons:
if polygon in holes:
continue
polygon_list = [polygon]
for other in polygons:
if polygon == other:
continue
if polygon.contains(other) and other not in holes:
if any(p.contains(other) or p.intersects(other) for p in polygon_list[1:]):
continue
holes.append(other)
# if possible let's make the hole a tiny little bit smaller, just in case, it hits the edge
# and would lead therefore to an invalid shape
o = other.buffer(-0.01)
if not o.is_empty and o.geom_type == 'Polygon':
other = o
polygon_list.append(other)
multipolygons.append(polygon_list)
return multipolygons
def polygons_to_nodes(self, polygon_list: List[List[Polygon]], element: EmbroideryElement) -> None:
# reverse the list of polygons, we don't want to cover smaller shapes
polygon_list = polygon_list[::-1]
parent = element.node.getparent()
assert parent is not None, "The element should be part of a group."
index = parent.index(element.node)
for polygons in polygon_list:
if polygons[0].area < 5:
continue
el = copy(element.node)
# Set fill-rule to evenodd
style = el.get('style', ' ').split(';')
style = [s for s in style if not s.startswith('fill-rule')]
style.append('fill-rule:evenodd;')
style = ';'.join(style)
el.set('style', style)
# update element id
if len(polygon_list) > 1:
node_id = self.uniqueId(el.get('id') + '_')
el.set('id', node_id)
# Set path
d = Path()
for polygon in polygons:
path = Path(polygon.exterior.coords)
path.close()
d += path
el.set('d', str(d))
el.set('transform', get_correction_transform(element.node))
parent.insert(index, el)
parent.remove(element.node)
|