tons of bug fixes (#364)

bug fixes
pull/389/head
Lex Neva 2019-02-16 17:07:34 -05:00 zatwierdzone przez GitHub
commit 2ab4c451e8
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
18 zmienionych plików z 219 dodań i 120 usunięć

Wyświetl plik

@ -1,8 +1,12 @@
import math
import traceback
from shapely import geometry as shgeo
from ..exceptions import InkstitchException
from ..i18n import _
from ..utils import cache
from ..stitches import auto_fill
from ..utils import cache
from .element import param, Patch
from .fill import Fill
@ -92,6 +96,18 @@ class AutoFill(Fill):
def fill_underlay_inset(self):
return self.get_float_param('fill_underlay_inset_mm', 0)
@property
@param(
'fill_underlay_skip_last',
_('Skip last stitch in each row'),
tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. '
'Skipping it decreases stitch count and density.'),
group=_('AutoFill Underlay'),
type='boolean',
default=False)
def fill_underlay_skip_last(self):
return self.get_boolean_param("fill_underlay_skip_last", False)
@property
@param('expand_mm',
_('Expand'),
@ -142,25 +158,42 @@ class AutoFill(Fill):
starting_point = self.get_starting_point(last_patch)
ending_point = self.get_ending_point()
if self.fill_underlay:
stitches.extend(auto_fill(self.underlay_shape,
self.fill_underlay_angle,
self.fill_underlay_row_spacing,
self.fill_underlay_row_spacing,
self.fill_underlay_max_stitch_length,
try:
if self.fill_underlay:
stitches.extend(auto_fill(self.underlay_shape,
self.fill_underlay_angle,
self.fill_underlay_row_spacing,
self.fill_underlay_row_spacing,
self.fill_underlay_max_stitch_length,
self.running_stitch_length,
self.staggers,
self.fill_underlay_skip_last,
starting_point))
starting_point = stitches[-1]
stitches.extend(auto_fill(self.fill_shape,
self.angle,
self.row_spacing,
self.end_row_spacing,
self.max_stitch_length,
self.running_stitch_length,
self.staggers,
starting_point))
starting_point = stitches[-1]
self.skip_last,
starting_point,
ending_point))
except InkstitchException, exc:
# for one of our exceptions, just print the message
self.fatal(_("Unable to autofill: ") + str(exc))
except Exception, exc:
# 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 += "\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 += traceback.format_exc()
stitches.extend(auto_fill(self.fill_shape,
self.angle,
self.row_spacing,
self.end_row_spacing,
self.max_stitch_length,
self.running_stitch_length,
self.staggers,
starting_point,
ending_point))
self.fatal(message)
return [Patch(stitches=stitches, color=self.color)]

Wyświetl plik

@ -1,15 +1,15 @@
import sys
from copy import deepcopy
import sys
from ..i18n import _
from ..utils import cache
from ..svg import PIXELS_PER_MM, convert_length, get_doc_size, apply_transforms
from ..commands import find_commands
# inkscape-provided utilities
import simplestyle
import cubicsuperpath
from cspsubdiv import cspsubdiv
import cubicsuperpath
import simplestyle
from ..commands import find_commands
from ..i18n import _
from ..svg import PIXELS_PER_MM, convert_length, get_doc_size, apply_transforms
from ..svg.tags import INKSCAPE_LABEL
from ..utils import cache
class Patch:
@ -274,7 +274,14 @@ class EmbroideryElement(object):
return patches
def fatal(self, message):
# L10N used when showing an error message to the user such as "satin column: One or more of the rungs doesn't
# intersect both rails."
print >> sys.stderr, self.node.get("id") + ":", _("error:"), message.encode("UTF-8")
label = self.node.get(INKSCAPE_LABEL)
id = self.node.get("id")
if label:
name = "%s (%s)" % (label, id)
else:
name = id
# L10N used when showing an error message to the user such as
# "Some Path (path1234): error: satin column: One or more of the rungs doesn't intersect both rails."
print >> sys.stderr, "%s: %s %s" % (name, _("error:"), message.encode("UTF-8"))
sys.exit(1)

Wyświetl plik

@ -1,11 +1,12 @@
from shapely import geometry as shgeo
import math
from .element import param, EmbroideryElement, Patch
from shapely import geometry as shgeo
from ..i18n import _
from ..stitches import legacy_fill
from ..svg import PIXELS_PER_MM
from ..utils import cache
from ..stitches import legacy_fill
from .element import param, EmbroideryElement, Patch
class Fill(EmbroideryElement):
@ -40,6 +41,17 @@ class Fill(EmbroideryElement):
# SVG spec says the default fill is black
return self.get_style("fill", "#000000")
@property
@param(
'skip_last',
_('Skip last stitch in each row'),
tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. '
'Skipping it decreases stitch count and density.'),
type='boolean',
default=False)
def skip_last(self):
return self.get_boolean_param("skip_last", False)
@property
@param(
'flip',
@ -133,5 +145,6 @@ class Fill(EmbroideryElement):
self.end_row_spacing,
self.max_stitch_length,
self.flip,
self.staggers)
self.staggers,
self.skip_last)
return [Patch(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists]

Wyświetl plik

@ -1,12 +1,13 @@
from itertools import chain, izip
from copy import deepcopy
from shapely import geometry as shgeo, affinity as shaffinity
import cubicsuperpath
from itertools import chain, izip
import cubicsuperpath
from shapely import geometry as shgeo, affinity as shaffinity
from .element import param, EmbroideryElement, Patch
from ..i18n import _
from ..utils import cache, Point, cut, collapse_duplicate_point
from ..svg import line_strings_to_csp, point_lists_to_csp
from ..utils import cache, Point, cut, collapse_duplicate_point
from .element import param, EmbroideryElement, Patch
class SatinColumn(EmbroideryElement):
@ -255,7 +256,7 @@ class SatinColumn(EmbroideryElement):
intersections += len(intersection)
break
elif not isinstance(intersection, shgeo.Point):
self.fatal("intersection is a: %s %s" % (intersection, intersection.geoms))
self.fatal("INTERNAL ERROR: intersection is: %s %s" % (intersection, getattr(intersection, 'geoms', None)))
else:
intersections += 1
@ -716,22 +717,22 @@ class SatinColumn(EmbroideryElement):
# First, verify that we have valid paths.
self.validate_satin_column()
patches = []
patch = Patch(color=self.color)
if self.center_walk_underlay:
patches.append(self.do_center_walk())
patch += self.do_center_walk()
if self.contour_underlay:
patches.append(self.do_contour_underlay())
patch += self.do_contour_underlay()
if self.zigzag_underlay:
# zigzag underlay comes after contour walk underlay, so that the
# zigzags sit on the contour walk underlay like rail ties on rails.
patches.append(self.do_zigzag_underlay())
patch += self.do_zigzag_underlay()
if self.e_stitch:
patches.append(self.do_e_stitch())
patch += self.do_e_stitch()
else:
patches.append(self.do_satin())
patch += self.do_satin()
return patches
return [patch]

Wyświetl plik

@ -0,0 +1,2 @@
class InkstitchException(Exception):
pass

Wyświetl plik

@ -1,17 +1,18 @@
import inkex
from shapely import geometry as shgeo
from itertools import chain, groupby
import numpy
from numpy import diff, sign, setdiff1d
import math
from copy import deepcopy
from itertools import chain, groupby
import math
import inkex
from numpy import diff, sign, setdiff1d
import numpy
from shapely import geometry as shgeo
from .base import InkstitchExtension
from ..svg.tags import SVG_PATH_TAG
from ..svg import get_correction_transform, PIXELS_PER_MM
from ..elements import Stroke
from ..utils import Point
from ..i18n import _
from ..svg import get_correction_transform, PIXELS_PER_MM
from ..svg.tags import SVG_PATH_TAG
from ..utils import Point
from .base import InkstitchExtension
class SelfIntersectionError(Exception):
@ -112,10 +113,10 @@ class ConvertToSatin(InkstitchExtension):
if not isinstance(left_rail, shgeo.LineString) or \
not isinstance(right_rail, shgeo.LineString):
# If the parallel offsets come out as anything but a LineString, that means the
# path intersects itself, when taking its stroke width into consideration. See
# the last example for parallel_offset() in the Shapely documentation:
# https://shapely.readthedocs.io/en/latest/manual.html#object.parallel_offset
# If the parallel offsets come out as anything but a LineString, that means the
# path intersects itself, when taking its stroke width into consideration. See
# the last example for parallel_offset() in the Shapely documentation:
# 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

Wyświetl plik

@ -6,6 +6,7 @@ from itertools import groupby
import os
import sys
import wx
from wx.lib.scrolledpanel import ScrolledPanel
@ -460,15 +461,16 @@ class Params(InkstitchExtension):
element = EmbroideryElement(node)
classes = []
if element.get_style("fill"):
classes.append(AutoFill)
classes.append(Fill)
if not is_command(node):
if element.get_style("fill", "black") is not None:
classes.append(AutoFill)
classes.append(Fill)
if element.get_style("stroke") and not is_command(node):
classes.append(Stroke)
if element.get_style("stroke") is not None:
classes.append(Stroke)
if element.get_style("stroke-dasharray") is None:
classes.append(SatinColumn)
if element.get_style("stroke-dasharray") is None:
classes.append(SatinColumn)
return classes

Wyświetl plik

@ -385,8 +385,8 @@ class DrawingPanel(wx.Panel):
self.go()
def choose_zoom_and_pan(self, event=None):
# ignore if called before we load the stitch plan
if not self.width and not self.height:
# ignore if EVT_SIZE fired before we load the stitch plan
if not self.width and not self.height and event is not None:
return
panel_width, panel_height = self.GetClientSize()

Wyświetl plik

@ -1,11 +1,13 @@
import sys
import pyembroidery
import sys
import simpletransform
from .i18n import _
from .utils import Point
from .svg import PIXELS_PER_MM, get_doc_size, get_viewbox_transform
from .commands import global_command
from .i18n import _
from .stitch_plan import Stitch
from .svg import PIXELS_PER_MM, get_doc_size, get_viewbox_transform
from .utils import Point
def get_command(stitch):
@ -57,6 +59,7 @@ def write_embroidery_file(file_path, stitch_plan, svg, settings={}):
origin = get_origin(svg)
pattern = pyembroidery.EmbPattern()
stitch = Stitch(0, 0)
for color_block in stitch_plan:
pattern.add_thread(color_block.color.pyembroidery_thread)
@ -99,5 +102,5 @@ def write_embroidery_file(file_path, stitch_plan, svg, settings={}):
except IOError as e:
# L10N low-level file error. %(error)s is (hopefully?) translated by
# the user's system automatically.
print >> sys.stderr, _("Error writing to %(path)s: %(error)s") % dict(path=file_path, error=e.message)
print >> sys.stderr, _("Error writing to %(path)s: %(error)s") % dict(path=file_path, error=e.strerror)
sys.exit(1)

Wyświetl plik

@ -1,16 +1,22 @@
import sys
import shapely
import networkx
from itertools import groupby, izip
from collections import deque
from itertools import groupby, izip
import sys
from .fill import intersect_region_with_grating, row_num, stitch_row
from .running_stitch import running_stitch
import networkx
import shapely
from ..exceptions import InkstitchException
from ..i18n import _
from ..utils.geometry import Point as InkstitchPoint, cut
from .fill import intersect_region_with_grating, row_num, stitch_row
from .running_stitch import running_stitch
class MaxQueueLengthExceeded(Exception):
class MaxQueueLengthExceeded(InkstitchException):
pass
class InvalidPath(InkstitchException):
pass
@ -39,16 +45,25 @@ class PathEdge(object):
return self.key == self.SEGMENT_KEY
def auto_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, running_stitch_length, staggers, starting_point, ending_point=None):
def auto_fill(shape,
angle,
row_spacing,
end_row_spacing,
max_stitch_length,
running_stitch_length,
staggers,
skip_last,
starting_point,
ending_point=None):
stitches = []
rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing)
segments = [segment for row in rows_of_segments for segment in row]
graph = build_graph(shape, segments, angle, row_spacing)
graph = build_graph(shape, segments, angle, row_spacing, max_stitch_length)
path = find_stitch_path(graph, segments, starting_point, ending_point)
stitches.extend(path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers))
stitches.extend(path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers, skip_last))
return stitches
@ -80,7 +95,7 @@ def project(shape, coords, outline_index):
return outline.project(shapely.geometry.Point(*coords))
def build_graph(shape, segments, angle, row_spacing):
def build_graph(shape, segments, angle, row_spacing, max_stitch_length):
"""build a graph representation of the grating segments
This function builds a specialized graph (as in graph theory) that will
@ -163,12 +178,21 @@ def build_graph(shape, segments, angle, row_spacing):
if i % 2 == edge_set:
graph.add_edge(node1, node2, key="extra")
if not networkx.is_eulerian(graph):
raise Exception(_("Unable to autofill. This most often happens because your shape is made up of multiple sections that aren't connected."))
check_graph(graph, shape, max_stitch_length)
return graph
def check_graph(graph, shape, max_stitch_length):
if networkx.is_empty(graph) or not networkx.is_eulerian(graph):
if shape.area < max_stitch_length ** 2:
raise InvalidPath(_("This shape is so small that it cannot be filled with rows of stitches. "
"It would probably look best as a satin column or running stitch."))
else:
raise InvalidPath(_("Cannot parse shape. "
"This most often happens because your shape is made up of multiple sections that aren't connected."))
def node_list_to_edge_list(node_list):
return zip(node_list[:-1], node_list[1:])
@ -317,14 +341,16 @@ def get_outline_nodes(graph, outline_index=0):
def find_initial_path(graph, starting_point, ending_point=None):
starting_node = nearest_node_on_outline(graph, starting_point)
if ending_point is None:
if ending_point is not None:
ending_node = nearest_node_on_outline(graph, ending_point)
if ending_point is None or starting_node is ending_node:
# If they didn't give an ending point, pick either neighboring node
# along the outline -- doesn't matter which. We do this because
# the algorithm requires we start with _some_ path.
neighbors = [n for n, keys in graph.adj[starting_node].iteritems() if 'outline' in keys]
return [PathEdge((starting_node, neighbors[0]), "initial")]
else:
ending_node = nearest_node_on_outline(graph, ending_point)
outline_nodes = get_outline_nodes(graph)
# Multiply the outline_nodes list by 2 (duplicate it) because
@ -513,7 +539,10 @@ def connect_points(shape, start, end, running_stitch_length, row_spacing):
# Now do running stitch along the path we've found. running_stitch() will
# avoid cutting sharp corners.
path = [InkstitchPoint(*p) for p in points]
return running_stitch(path, running_stitch_length)
stitches = running_stitch(path, running_stitch_length)
# The row of stitches already stitched the first point, so skip it.
return stitches[1:]
def trim_end(path):
@ -521,14 +550,14 @@ def trim_end(path):
path.pop()
def path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers):
def path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers, skip_last):
path = collapse_sequential_outline_edges(graph, path)
stitches = []
for edge in path:
if edge.is_segment():
stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers)
stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers, skip_last)
else:
stitches.extend(connect_points(shape, edge[0], edge[1], running_stitch_length, row_spacing))

Wyświetl plik

@ -242,7 +242,7 @@ class RunningStitch(object):
@cache
def reversed(self):
return RunningStitch(shgeo.LineString(reversed(self.path.coords)), self.style)
return RunningStitch(shgeo.LineString(reversed(self.path.coords)), self.original_element)
def is_sequential(self, other):
if not isinstance(other, RunningStitch):

Wyświetl plik

@ -1,15 +1,16 @@
import shapely
import math
import shapely
from ..svg import PIXELS_PER_MM
from ..utils import cache, Point as InkstitchPoint
def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers):
def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers, skip_last):
rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing, flip)
groups_of_segments = pull_runs(rows_of_segments, shape, row_spacing)
return [section_to_stitches(group, angle, row_spacing, max_stitch_length, staggers)
return [section_to_stitches(group, angle, row_spacing, max_stitch_length, staggers, skip_last)
for group in groups_of_segments]
@ -37,7 +38,7 @@ def adjust_stagger(stitch, angle, row_spacing, max_stitch_length, staggers):
return stitch - offset * east(angle)
def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers):
def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers, skip_last=False):
# We want our stitches to look like this:
#
# ---*-----------*-----------
@ -81,7 +82,7 @@ def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, stagge
stitches.append(beg + offset * row_direction)
offset += max_stitch_length
if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM:
if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM and not skip_last:
stitches.append(end)
@ -164,7 +165,7 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
return rows
def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length, staggers):
def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length, staggers, skip_last):
stitches = []
swap = False
@ -174,7 +175,7 @@ def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length
if (swap):
(beg, end) = (end, beg)
stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers)
stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers, skip_last)
swap = not swap

Wyświetl plik

@ -23,7 +23,7 @@ def running_stitch(points, stitch_length):
segment_start = points[0]
last_segment_direction = None
# This tracks the distance we've travelled along the current segment so
# This tracks the distance we've traveled along the current segment so
# far. Each time we make a stitch, we add the stitch_length to this
# value. If we fall off the end of the current segment, we carry over
# the remainder to the next segment.
@ -62,8 +62,12 @@ def running_stitch(points, stitch_length):
last_segment_direction = segment_direction
distance -= segment_length
# stitch a single point if the path has a length of zero
if not output:
output.append(segment_start)
# stitch the last point unless we're already almost there
if (segment_start - output[-1]).length() > 0.1:
if (segment_start - output[-1]).length() > 0.1 or len(output) == 0:
output.append(segment_start)
return output

Wyświetl plik

@ -1,12 +1,12 @@
import simpletransform
import simplestyle
import inkex
import simplestyle
import simpletransform
from .units import get_viewbox_transform
from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG, SVG_DEFS_TAG
from .realistic_rendering import realistic_stitch, realistic_filter
from ..i18n import _
from ..utils import cache
from .realistic_rendering import realistic_stitch, realistic_filter
from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG, SVG_DEFS_TAG
from .units import get_viewbox_transform
def color_block_to_point_lists(color_block):
@ -21,6 +21,9 @@ def color_block_to_point_lists(color_block):
if not stitch.jump and not stitch.color_change:
point_lists[-1].append(stitch.as_tuple())
# filter out empty point lists
point_lists = [p for p in point_lists if p]
return point_lists

Wyświetl plik

@ -1,6 +1,7 @@
from shapely.geometry import LineString, Point as ShapelyPoint
import math
from shapely.geometry import LineString, Point as ShapelyPoint
def cut(line, distance, normalized=False):
""" Cuts a LineString in two at a distance from its starting point.
@ -51,9 +52,8 @@ def cut_path(points, length):
def collapse_duplicate_point(geometry):
if hasattr(geometry, 'geoms'):
if geometry.area < 0.01:
return geometry.representative_point()
if geometry.area < 0.01:
return geometry.representative_point()
return geometry

@ -1 +1 @@
Subproject commit d26e280020bbe1d2b5af977b9cd74f60db8be71b
Subproject commit 47b795a084bdc3281fbf944b940609bf86193fd8

Wyświetl plik

@ -5,7 +5,7 @@ networkx==2.2
shapely
lxml
appdirs
numpy
numpy<1.16.0
jinja2
requests
colormath

12
stub.py
Wyświetl plik

@ -1,10 +1,11 @@
#!/usr/bin/env python
import sys
import os
import subprocess
import sys
import traceback
# ink/stitch
#
# stub.py: pyinstaller execution stub
@ -15,7 +16,6 @@ import traceback
# This Python script exists only to execute the actual extension binary. It
# can be copied to, e.g., "embroider_params.py", in which case it will look
# for a binary at inkstitch/bin/embroider_params.
script_name = os.path.basename(__file__)
if script_name.endswith('.py'):
@ -48,12 +48,12 @@ if sys.platform == "win32":
import msvcrt
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
stdout = stdout.strip()
if stdout:
print stdout.strip(),
sys.stdout.write(stdout)
sys.stdout.flush()
stderr = stderr.strip()
if stderr:
print >> sys.stderr, stderr.strip(),
sys.stderr.write(stderr.strip())
sys.stderr.flush()
sys.exit(extension.returncode)