break apart loops (#690)

pull/708/head
Kaalleen 2020-05-16 23:12:06 +02:00 zatwierdzone przez GitHub
rodzic a308db7ae1
commit e03b032f85
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
9 zmienionych plików z 141 dodań i 54 usunięć

Wyświetl plik

@ -19,7 +19,7 @@ class UnconnectedError(ValidationError):
"Ink/Stitch doesn't know what order to stitch them in. Please break this "
"object up into separate shapes.")
steps_to_solve = [
_('* Extensions > Ink/Stitch > Fill Tools > Break Apart and Retain Holes.')
_('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects'),
]
@ -27,15 +27,7 @@ class InvalidShapeError(ValidationError):
name = _("Border crosses itself")
description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.")
steps_to_solve = [
_("1. Inkscape has a limit to how far it lets you zoom in. Sometimes there can be a little loop, "
"that's so small, you can't see it, but Ink/Stitch can. It's especially common for Inkscape's "
"Trace Bitmap to produce those tiny loops."),
_("* Delete the node"),
_("* Or try to adjust it's handles"),
_("2. If you can actually see a loop, run the following commands to seperate the crossing shapes:"),
_("* Path > Union (Ctrl++)"),
_("* Path > Break apart (Shift+Ctrl+K)"),
_("* (Optional) Recombine shapes with holes (Ctrl+K).")
_('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects')
]

Wyświetl plik

@ -1,68 +1,146 @@
from copy import deepcopy
import logging
from copy import copy
from shapely.geometry import Polygon
from shapely.geometry import LineString, MultiPolygon, Polygon
from shapely.ops import polygonize, unary_union
import inkex
from ..elements import AutoFill, Fill
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):
def effect(self): # noqa: C901
if not self.get_elements():
return
'''
This will break apart fill areas into separate elements.
'''
def __init__(self, *args, **kwargs):
InkstitchExtension.__init__(self, *args, **kwargs)
self.OptionParser.add_option("-m", "--method", type="int", default=1, dest="method")
def effect(self):
if not self.selected:
inkex.errormsg(_("Please select one or more fill areas to break apart."))
return
for element in self.elements:
if not isinstance(element, AutoFill) and not isinstance(element, Fill):
continue
if len(element.paths) <= 1:
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
polygons = []
multipolygons = []
holes = []
# we don't want to touch valid elements
paths = element.flatten(element.parse_path())
paths.sort(key=lambda point_list: Polygon(point_list).area, reverse=True)
polygon = MultiPolygon([(paths[0], paths[1:])])
if self.geom_is_valid(polygon):
continue
for path in element.paths:
polygons.append(Polygon(path))
polygons = self.break_apart_paths(paths)
polygons = self.ensure_minimum_size(polygons, 5)
if self.options.method == 1:
polygons = self.combine_overlapping_polygons(polygons)
polygons = self.recombine_polygons(polygons)
if polygons:
self.polygons_to_nodes(polygons, element)
# sort paths by size and convert to polygons
polygons.sort(key=lambda polygon: polygon.area, reverse=True)
def break_apart_paths(self, paths):
polygons = []
for path in paths:
linestring = LineString(path)
polygon = Polygon(path).buffer(0)
if not linestring.is_simple:
linestring = unary_union(linestring)
for polygon in polygonize(linestring):
polygons.append(polygon)
else:
polygons.append(polygon)
return polygons
for shape in polygons:
if shape in holes:
def combine_overlapping_polygons(self, polygons):
for polygon in polygons:
for other in polygons:
if polygon == other:
continue
polygon_list = [shape]
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
for other in polygons:
if shape != other and shape.contains(other) and other not in holes:
# check if "other" is inside a hole, before we add it to the list
if any(p.contains(other) for p in polygon_list[1:]):
continue
polygon_list.append(other)
holes.append(other)
multipolygons.append(polygon_list)
self.element_to_nodes(multipolygons, element)
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 element_to_nodes(self, multipolygons, element):
for polygons in multipolygons:
el = deepcopy(element)
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 = []
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)
d = ""
for polygon in polygons:
# copy element and replace path
el.node.set('id', self.uniqueId(element.node.get('id') + "_"))
# update element id
if len(polygon_list) > 1:
node_id = self.uniqueId(el.get('id') + '_')
el.set('id', node_id)
d += "M"
for x, y in polygon.exterior.coords:
d += "%s,%s " % (x, y)
d += " "
d += "Z"
el.node.set('d', d)
el.node.set('transform', get_correction_transform(element.node))
element.node.getparent().insert(0, el.node)
el.set('d', d)
el.set('transform', get_correction_transform(element.node))
element.node.getparent().insert(index, el)
element.node.getparent().remove(element.node)

Wyświetl plik

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>{% trans %}Break Apart and Retain Holes{% endtrans %}</name>
<name>{% trans %}Break Apart Fill Objects{% endtrans %}</name>
<id>org.inkstitch.break_apart.{{ locale }}</id>
<param name="extension" type="string" gui-hidden="true">break_apart</param>
<effect>
@ -11,6 +11,13 @@
</submenu>
</effects-menu>
</effect>
<param name="description" type="description">
{% trans %}This extension will try to repair fill shapes and break them apart if necessary. Holes will be retained. Use on simple or overlapping shapes.{% endtrans %}
</param>
<param name="method" type="optiongroup" _gui-text="Method">
<option value="0">Simple</option>
<option value="1">Complex</option>
</param>
<script>
{{ command_tag | safe }}
</script>

Wyświetl plik

@ -17,7 +17,9 @@
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" />
<submenu name="Ink/Stitch">
<submenu name="{% trans %}Visualise and Export{% endtrans %}" />
</submenu>
</effects-menu>
</effect>
<script>

Wyświetl plik

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>{% trans %}Print / Realistic Preview{% endtrans %}</name>
<name>{% trans %}PDF Export{% endtrans %}</name>
<id>org.inkstitch.print.{{ locale }}</id>
<param name="extension" type="string" gui-hidden="true">print</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" />
<submenu name="Ink/Stitch">
<submenu name="{% trans %}Visualise and Export{% endtrans %}" />
</submenu>
</effects-menu>
</effect>
<script>

Wyświetl plik

@ -13,6 +13,7 @@
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch">
<submenu name="{% trans %}Troubleshoot{% endtrans %}" />
</submenu>
</effects-menu>
</effect>

Wyświetl plik

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>{% trans %}Simulator{% endtrans %}</name>
<name>{% trans %}Simulator / Realistic Preview{% endtrans %}</name>
<id>org.inkstitch.simulator.{{ locale }}</id>
<param name="extension" type="string" gui-hidden="true">simulator</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" />
<submenu name="Ink/Stitch">
<submenu name="{% trans %}Visualise and Export{% endtrans %}" />
</submenu>
</effects-menu>
</effect>
<script>

Wyświetl plik

@ -6,7 +6,9 @@
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" />
<submenu name="Ink/Stitch">
<submenu name="{% trans %}Visualise and Export{% endtrans %}" />
</submenu>
</effects-menu>
</effect>
<script>

Wyświetl plik

@ -7,6 +7,7 @@
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch">
<submenu name="{% trans %}Troubleshoot{% endtrans %}" />
</submenu>
</effects-menu>
</effect>