* dont fail on satin with fill

* fill stitch error message

* convert to satin mac issue

* auto_satin: add rung for two node old style satins

* avoid divide by zero in intersect_region_with_grating

* fix for incorrect stagger in guided fill

* better rail sectioning algorithm

* fix #1780

* fix #1816

Co-authored-by: Lex Neva
pull/1923/head
Kaalleen 2022-11-27 08:37:59 +01:00 zatwierdzone przez GitHub
rodzic 9c0b64560c
commit e9278c55c3
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
10 zmienionych plików z 101 dodań i 118 usunięć

Wyświetl plik

@ -10,8 +10,8 @@ from .validation import ObjectTypeWarning
class EmptyD(ObjectTypeWarning):
name = _("Empty D-Attribute")
description = _("There is an invalid path object in the document, the d-attribute is missing.")
name = _("Empty Path")
description = _("There is an invalid object in the document without geometry information.")
steps_to_solve = [
_('* Run Extensions > Ink/Stitch > Troubleshoot > Cleanup Document...')
]

Wyświetl plik

@ -272,6 +272,8 @@ class FillStitch(EmbroideryElement):
return shgeo.MultiPolygon([valid_shape])
if isinstance(valid_shape, shgeo.LineString):
return shgeo.MultiPolygon([])
if shape.area == 0:
return shgeo.MultiPolygon([])
polygons = []
for polygon in valid_shape.geoms:
@ -713,12 +715,17 @@ class FillStitch(EmbroideryElement):
# for an uncaught exception, give a little more info so that they can create a bug report
message = ""
message += _("Error during autofill! This means that there is a problem with Ink/Stitch.")
message += _("Error during autofill! This means it is a bug in Ink/Stitch.")
message += "\n\n"
# L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new
message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ")
message += "https://github.com/inkstitch/inkstitch/issues/new\n\n"
message += version.get_inkstitch_version() + "\n\n"
message += _("If you'd like to help please\n"
"- copy the entire error message below\n"
"- save your SVG file and\n"
"- create a new issue at")
message += " https://github.com/inkstitch/inkstitch/issues/new\n\n"
message += _("Include the error description and also (if possible) the svg file.")
message += '\n\n\n'
message += version.get_inkstitch_version() + '\n'
message += traceback.format_exc()
self.fatal(message)

Wyświetl plik

@ -7,7 +7,6 @@ from copy import deepcopy
from itertools import chain
import numpy as np
from shapely import affinity as shaffinity
from shapely import geometry as shgeo
from shapely.ops import nearest_points
@ -16,23 +15,11 @@ from inkex import paths
from ..i18n import _
from ..stitch_plan import StitchGroup
from ..svg import line_strings_to_csp, point_lists_to_csp
from ..utils import Point, cache, collapse_duplicate_point, cut
from ..utils import Point, cache, cut, cut_multiple
from .element import EmbroideryElement, param, PIXELS_PER_MM
from .validation import ValidationError, ValidationWarning
class SatinHasFillError(ValidationError):
name = _("Satin column has fill")
description = _("Satin column: Object has a fill (but should not)")
steps_to_solve = [
_("* Select this object."),
_("* Open the Fill and Stroke panel"),
_("* Open the Fill tab"),
_("* Disable the Fill"),
_("* Alternative: open Params and switch this path to Stroke to disable Satin Column mode")
]
class TooFewPathsError(ValidationError):
name = _("Too few subpaths")
description = _("Satin column: Object has too few subpaths. A satin column should have at least two subpaths (the rails).")
@ -338,24 +325,6 @@ class SatinColumn(EmbroideryElement):
@cache
def flattened_rungs(self):
"""The rungs, as LineStrings."""
rungs = []
for rung in self._raw_rungs:
# make sure each rung intersects both rails
if not rung.intersects(self.flattened_rails[0]) or not rung.intersects(self.flattened_rails[1]):
# the rung does not intersect both rails
# get nearest points on rungs
start = nearest_points(rung, self.flattened_rails[0])[1]
end = nearest_points(rung, self.flattened_rails[1])[1]
# extend from the nearest points just a little bit to make sure that we get an intersection
rung = shaffinity.scale(shgeo.LineString([start, end]), 1.01, 1.01)
rungs.append(rung)
else:
rungs.append(rung)
return tuple(rungs)
@property
@cache
def _raw_rungs(self):
return tuple(shgeo.LineString(self.flatten_subpath(rung)) for rung in self.rungs)
@property
@ -379,21 +348,18 @@ class SatinColumn(EmbroideryElement):
for rail in self.rails:
points = self.strip_control_points(rail)
# ignore the start and end
points = points[1:-1]
if len(points) > 2:
# Don't bother putting rungs at the start and end.
points = points[1:-1]
else:
# But do include one at the start if we wouldn't add one otherwise.
# This avoids confusing other parts of the code.
points = points[:-1]
rung_endpoints.append(points)
rungs = []
for start, end in zip(*rung_endpoints):
# Expand the points just a bit to ensure that shapely thinks they
# intersect with the rails even with floating point inaccuracy.
start = Point(*start)
end = Point(*end)
start, end = self.offset_points(start, end, (0.01, 0.01), (0, 0))
start = list(start)
end = list(end)
rungs.append([[start, start, start], [end, end, end]])
return rungs
@ -446,39 +412,22 @@ class SatinColumn(EmbroideryElement):
indices_by_length = sorted(list(range(num_paths)), key=lambda index: paths[index].length, reverse=True)
return indices_by_length[:2]
def _cut_rail(self, rail, rung):
for segment_index, rail_segment in enumerate(rail[:]):
if rail_segment is None:
continue
intersection = rail_segment.intersection(rung)
# If there are duplicate points in a rung-less satin, then
# intersection will be a GeometryCollection of multiple copies
# of the same point. This reduces it that to a single point.
intersection = collapse_duplicate_point(intersection)
if not intersection.is_empty:
cut_result = cut(rail_segment, rail_segment.project(intersection))
rail[segment_index:segment_index + 1] = cut_result
if cut_result[1] is None:
# if we were exactly at the end of one of the existing rail segments,
# stop here or we'll get a spurious second intersection on the next
# segment
break
@property
@cache
def flattened_sections(self):
"""Flatten the rails, cut with the rungs, and return the sections in pairs."""
rails = [[rail] for rail in self.flattened_rails]
rails = list(self.flattened_rails)
rungs = self.flattened_rungs
for rung in rungs:
for rail in rails:
self._cut_rail(rail, rung)
for i, rail in enumerate(rails):
cut_points = []
for rung in rungs:
point_on_rung, point_on_rail = nearest_points(rung, rail)
cut_points.append(rail.project(point_on_rail))
rails[i] = cut_multiple(rail, cut_points)
for rail in rails:
for i in range(len(rail)):
@ -502,19 +451,15 @@ class SatinColumn(EmbroideryElement):
return sections
def validation_warnings(self):
for rung in self._raw_rungs:
for rung in self.flattened_rungs:
for rail in self.flattened_rails:
intersection = rung.intersection(rail)
if intersection.is_empty:
yield DanglingRungWarning(rung.interpolate(0.5, normalized=True))
def validation_errors(self):
# The node should have exactly two paths with no fill. Each
# path should have the same number of points, meaning that they
# will both be made up of the same number of bezier curves.
if self.get_style("fill") is not None:
yield SatinHasFillError(self.shape.centroid)
# The node should have exactly two paths with the same number of points - or it should
# have two rails and at least one rung
if len(self.rails) < 2:
yield TooFewPathsError(self.shape.centroid)
@ -522,7 +467,7 @@ class SatinColumn(EmbroideryElement):
if len(self.rails[0]) != len(self.rails[1]):
yield UnequalPointsError(self.flattened_rails[0].interpolate(0.5, normalized=True))
else:
for rung in self._raw_rungs:
for rung in self.flattened_rungs:
for rail in self.flattened_rails:
intersection = rung.intersection(rail)
if not intersection.is_empty and not isinstance(intersection, shgeo.Point):
@ -664,14 +609,10 @@ class SatinColumn(EmbroideryElement):
for path_list in path_lists:
if len(path_list) in (2, 4):
# Add the rung just after the start of the satin.
rung_start = path_list[0].interpolate(0.1)
rung_end = path_list[1].interpolate(0.1)
# Add the rung at the start of the satin.
rung_start = path_list[0].coords[0]
rung_end = path_list[1].coords[0]
rung = shgeo.LineString((rung_start, rung_end))
# make it a bit bigger so that it definitely intersects
rung = shaffinity.scale(rung, 1.1, 1.1)
path_list.append(rung)
def _path_list_to_satins(self, path_list):

Wyświetl plik

@ -6,11 +6,11 @@
from ..commands import is_command
from ..marker import has_marker
from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_PATH_TAG,
SVG_POLYLINE_TAG, SVG_TEXT_TAG)
from .fill_stitch import FillStitch
SVG_POLYGON_TAG, SVG_POLYLINE_TAG, SVG_TEXT_TAG)
from .clone import Clone, is_clone
from .element import EmbroideryElement
from .empty_d_object import EmptyDObject
from .fill_stitch import FillStitch
from .image import ImageObject
from .marker import MarkerObject
from .polyline import Polyline
@ -27,7 +27,8 @@ def node_to_elements(node, clone_to_element=False): # noqa: C901
# clone_to_element: get an actual embroiderable element once a clone has been defined as a clone
return [Clone(node)]
elif node.tag == SVG_PATH_TAG and not node.get('d', ''):
elif ((node.tag == SVG_PATH_TAG and not node.get('d', None)) or
(node.tag in [SVG_POLYLINE_TAG, SVG_POLYGON_TAG] and not node.get('points', None))):
return [EmptyDObject(node)]
elif has_marker(node):
@ -36,18 +37,17 @@ def node_to_elements(node, clone_to_element=False): # noqa: C901
elif node.tag in EMBROIDERABLE_TAGS or is_clone(node):
element = EmbroideryElement(node)
if element.get_boolean_param("satin_column") and element.get_style("stroke"):
return [SatinColumn(node)]
else:
elements = []
if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0":
elements.append(FillStitch(node))
if element.get_style("stroke"):
if not is_command(element.node):
elements.append(Stroke(node))
if element.get_boolean_param("stroke_first", False):
elements.reverse()
return elements
elements = []
if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0":
elements.append(FillStitch(node))
if element.get_style("stroke"):
if element.get_boolean_param("satin_column"):
elements.append(SatinColumn(node))
elif not is_command(element.node):
elements.append(Stroke(node))
if element.get_boolean_param("stroke_first", False):
elements.reverse()
return elements
elif node.tag == SVG_IMAGE_TAG:
return [ImageObject(node)]

Wyświetl plik

@ -129,11 +129,16 @@ class ConvertToSatin(InkstitchExtension):
if Point(*path[0]).distance(Point(*path[-1])) < 1:
raise SelfIntersectionError()
# Shapely is supposed to return right sided offsets in reversed direction, which it does, except for macOS.
# To avoid direction checking, we are going to rely on left side offsets only.
# Therefore we need to reverse the original path.
reversed_path = shgeo.LineString(reversed(path))
path = shgeo.LineString(path)
distance = stroke_width / 2.0
try:
left_rail = path.parallel_offset(stroke_width / 2.0, 'left', **style_args)
right_rail = path.parallel_offset(stroke_width / 2.0, 'right', **style_args)
left_rail = path.parallel_offset(distance, 'left', **style_args)
right_rail = reversed_path.parallel_offset(distance, 'left', **style_args)
except ValueError:
# TODO: fix this error automatically
# Error reference: https://github.com/inkstitch/inkstitch/issues/964
@ -149,7 +154,6 @@ class ConvertToSatin(InkstitchExtension):
# https://shapely.readthedocs.io/en/latest/manual.html#object.parallel_offset
raise SelfIntersectionError()
# for whatever reason, shapely returns a right-side offset's coordinates in reverse
left_rail = list(left_rail.coords)
right_rail = list(reversed(right_rail.coords))

Wyświetl plik

@ -81,6 +81,8 @@ class SatinSegment(object):
satin = satin.apply_transform()
_ensure_even_repeats(satin)
return satin
to_element = to_satin
@ -507,7 +509,6 @@ def name_elements(new_elements, preserve_order):
for element in new_elements:
if isinstance(element, SatinColumn):
element.node.set("id", generate_unique_id(element.node, "autosatin"))
_ensure_even_repeats(element)
else:
element.node.set("id", generate_unique_id(element.node, "autosatinrun"))
@ -515,7 +516,6 @@ def name_elements(new_elements, preserve_order):
if isinstance(element, SatinColumn):
# L10N Label for a satin column created by Auto-Route Satin Columns and Lettering extensions
element.node.set(INKSCAPE_LABEL, _("AutoSatin %d") % index)
_ensure_even_repeats(element)
else:
# L10N Label for running stitch (underpathing) created by Auto-Route Satin Columns amd Lettering extensions
element.node.set(INKSCAPE_LABEL, _("AutoSatin Running Stitch %d") % index)

Wyświetl plik

@ -125,6 +125,9 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
end -= center.y
height = abs(end - start)
if height == 0:
# return early to avoid divide-by-zero later
return []
# print >> dbg, "grating:", start, end, height, row_spacing, end_row_spacing

Wyświetl plik

@ -247,7 +247,7 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, num_stagge
debug.log_line_string(offset_line, f"offset {row}")
stitched_line = apply_stitches(offset_line, max_stitch_length, num_staggers, row_spacing, row * direction)
stitched_line = apply_stitches(offset_line, max_stitch_length, num_staggers, row_spacing, row)
intersection = shape.intersection(stitched_line)
if shape_envelope.intersects(stitched_line):

Wyświetl plik

@ -7,9 +7,10 @@ import colorsys
import re
import tinycss2.color3
from inkex import Color
from pyembroidery.EmbThread import EmbThread
from inkex import Color
class ThreadColor(object):
hex_str_re = re.compile('#([0-9a-z]{3}|[0-9a-z]{6})', re.I)

Wyświetl plik

@ -39,6 +39,40 @@ def cut(line, distance, normalized=False):
LineString([(cp.x, cp.y)] + coords[i:])]
def cut_multiple(line, distances, normalized=False):
"""Cut a LineString at multiple distances along that line.
Always returns a list of N + 1 members, where N is the number of distances
provided. Some members of the list may be None, indicating an empty
segment. This can happen if one of the distances is at the start or end
of the line, or if duplicate distances are provided.
Returns:
a list of LineStrings or None values"""
distances = list(sorted(distances))
segments = [line]
distance_so_far = 0
nones = []
for distance in distances:
segment = segments.pop()
before, after = cut(segment, distance - distance_so_far, normalized)
segments.append(before)
if after is None:
nones.append(after)
else:
if before is not None:
distance_so_far += before.length
segments.append(after)
segments.extend(nones)
return segments
def roll_linear_ring(ring, distance, normalized=False):
"""Make a linear ring start at a different point.
@ -113,13 +147,6 @@ def cut_path(points, length):
return [Point(*point) for point in subpath.coords]
def collapse_duplicate_point(geometry):
if geometry.area < 0.01:
return geometry.representative_point()
return geometry
class Point:
def __init__(self, x, y):
self.x = x