# 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.selected: 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: 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)