summaryrefslogtreecommitdiff
path: root/lib/extensions/break_apart.py
blob: 581e49bcb99d471f30b6053ed7bc4be2ee2a4f00 (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
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
163
164
165
166
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later.  See the file LICENSE for details.

import logging
from copy import copy

import inkex
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("-m", "--method", type=int, default=1, dest="method")
        self.minimum_size = 5

    def effect(self):  # noqa: C901
        if not self.svg.selection:
            inkex.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):
        polygons = []
        for path in paths:
            if len(path) < 3:
                continue
            linearring = LinearRing(path)
            if not linearring.is_simple:
                linearring = unary_union(linearring)
                for polygon in polygonize(linearring):
                    polygons.append(polygon)
            else:
                polygon = Polygon(path).buffer(0)
                polygons.append(polygon)
        return polygons

    def combine_overlapping_polygons(self, polygons):
        for polygon in polygons:
            for other in polygons:
                if polygon == other:
                    continue
                if polygon.overlaps(other):
                    diff = polygon.symmetric_difference(other)
                    if diff.geom_type == '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):
        # Don't complain about invalid shapes, we just want to know
        logger = logging.getLogger('shapely.geos')
        level = logger.level
        logger.setLevel(logging.CRITICAL)
        valid = geom.is_valid
        logger.setLevel(level)
        return valid

    def ensure_minimum_size(self, polygons, size):
        for polygon in polygons:
            if polygon.area < size:
                polygons.remove(polygon)
        return polygons

    def recombine_polygons(self, polygons):
        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, element):
        # reverse the list of polygons, we don't want to cover smaller shapes
        polygon_list = polygon_list[::-1]
        index = element.node.getparent().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 = ""
            for polygon in polygons:
                d += "M"
                for x, y in polygon.exterior.coords:
                    d += "%s,%s " % (x, y)
                    d += " "
                d += "Z"
            el.set('d', d)
            el.set('transform', get_correction_transform(element.node))
            element.node.getparent().insert(index, el)
        element.node.getparent().remove(element.node)