inkstitch/lib/extensions/break_apart.py

167 wiersze
6.2 KiB
Python
Czysty Zwykły widok Historia

2021-03-12 04:17:19 +00:00
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
2020-05-16 21:12:06 +00:00
import logging
from copy import copy
import inkex
from shapely.geometry import LinearRing, MultiPolygon, Polygon
2020-05-16 21:12:06 +00:00
from shapely.ops import polygonize, unary_union
2020-05-16 21:12:06 +00:00
from ..elements import EmbroideryElement
from ..i18n import _
from ..svg import get_correction_transform
2020-05-16 21:12:06 +00:00
from ..svg.tags import SVG_PATH_TAG
from .base import InkstitchExtension
class BreakApart(InkstitchExtension):
2020-05-16 21:12:06 +00:00
'''
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
2020-06-01 17:58:52 +00:00
def effect(self): # noqa: C901
if not self.svg.selection:
inkex.errormsg(_("Please select one or more fill areas to break apart."))
return
2020-05-16 21:12:06 +00:00
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
2020-05-16 21:12:06 +00:00
# we don't want to touch valid elements
paths = element.flatten(element.parse_path())
2020-06-01 17:58:52 +00:00
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:
2020-06-01 17:58:52 +00:00
continue
except ValueError:
pass
2020-05-16 21:12:06 +00:00
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)
2020-05-16 21:12:06 +00:00
def break_apart_paths(self, paths):
polygons = []
for path in paths:
2020-06-01 17:58:52 +00:00
if len(path) < 3:
continue
linearring = LinearRing(path)
if not linearring.is_simple:
linearring = unary_union(linearring)
for polygon in polygonize(linearring):
2020-05-16 21:12:06 +00:00
polygons.append(polygon)
else:
2020-06-01 17:58:52 +00:00
polygon = Polygon(path).buffer(0)
2020-05-16 21:12:06 +00:00
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)
2022-05-07 01:03:56 +00:00
for p in diff.geoms:
2020-05-16 21:12:06 +00:00
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
2020-05-16 21:12:06 +00:00
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
2020-05-16 21:12:06 +00:00
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)
2020-05-16 21:12:06 +00:00
for polygon in polygons:
if polygon in holes:
continue
polygon_list = [polygon]
for other in polygons:
if polygon == other:
continue
2020-05-16 21:12:06 +00:00
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)
2020-12-22 16:37:11 +00:00
# Set fill-rule to evenodd
2021-07-21 15:16:17 +00:00
style = el.get('style', ' ').split(';')
2020-12-22 16:37:11 +00:00
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"
2020-05-16 21:12:06 +00:00
el.set('d', d)
el.set('transform', get_correction_transform(element.node))
element.node.getparent().insert(index, el)
element.node.getparent().remove(element.node)