diff --git a/lib/elements/element.py b/lib/elements/element.py index 10b1852ad..e85657cd1 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -283,5 +283,6 @@ class EmbroideryElement(object): # 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")) + error_msg = "%s: %s %s" % (name, _("error:"), message) + print >> sys.stderr, "%s" % (error_msg.encode("UTF-8")) sys.exit(1) diff --git a/lib/elements/fill.py b/lib/elements/fill.py index 357adf4b7..7ccf7b27e 100644 --- a/lib/elements/fill.py +++ b/lib/elements/fill.py @@ -1,6 +1,7 @@ import math from shapely import geometry as shgeo +from shapely.validation import explain_validity from ..i18n import _ from ..stitches import legacy_fill @@ -104,37 +105,23 @@ class Fill(EmbroideryElement): @property @cache def shape(self): - poly_ary = [] - for sub_path in self.paths: - point_ary = [] - last_pt = None - for pt in sub_path: - if (last_pt is not None): - vp = (pt[0] - last_pt[0], pt[1] - last_pt[1]) - dp = math.sqrt(math.pow(vp[0], 2.0) + math.pow(vp[1], 2.0)) - # dbg.write("dp %s\n" % dp) - if (dp > 0.01): - # I think too-close points confuse shapely. - point_ary.append(pt) - last_pt = pt - else: - last_pt = pt - if len(point_ary) > 2: - poly_ary.append(point_ary) - - if not poly_ary: - self.fatal(_("shape %s is so small that it cannot be filled with stitches. Please make it bigger or delete it.") % self.node.get('id')) - # shapely's idea of "holes" are to subtract everything in the second set # from the first. So let's at least make sure the "first" thing is the # biggest path. - # TODO: actually figure out which things are holes and which are shells - poly_ary.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) - - polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])]) + paths = self.paths + paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) if not polygon.is_valid: - self.fatal(_("shape is not valid. This can happen if the border crosses over itself.")) + why = explain_validity(polygon) + + # I Wish this weren't so brittle... + if "Hole lies outside shell" in why: + self.fatal(_("this object is made up of unconnected shapes. This is not allowed because " + "Ink/Stitch doesn't know what order to stitch them in. Please break this " + "object up into separate shapes.")) + else: + self.fatal(_("shape is not valid. This can happen if the border crosses over itself.")) return polygon diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index bfa393847..d3c4d3d3a 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -325,6 +325,10 @@ class SatinColumn(EmbroideryElement): self.fatal(_("satin column: object %s has a fill (but should not)") % node_id) if not self.rungs: + if len(self.rails) < 2: + self.fatal(_("satin column: object %(id)s has too few paths. A satin column should have at least two paths (the rails).") % + dict(id=node_id)) + if len(self.rails[0]) != len(self.rails[1]): self.fatal(_("satin column: object %(id)s has two paths with an unequal number of points (%(length1)d and %(length2)d)") % dict(id=node_id, length1=len(self.rails[0]), length2=len(self.rails[1]))) diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 87dfa877b..5c71de2e7 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -16,7 +16,7 @@ def node_to_elements(node): elif node.tag == SVG_PATH_TAG: element = EmbroideryElement(node) - if element.get_boolean_param("satin_column"): + if element.get_boolean_param("satin_column") and element.get_style("stroke"): return [SatinColumn(node)] else: elements = [] diff --git a/lib/extensions/embroider.py b/lib/extensions/embroider.py index 1a5780315..b90896120 100644 --- a/lib/extensions/embroider.py +++ b/lib/extensions/embroider.py @@ -39,10 +39,14 @@ class Embroider(InkstitchExtension): def get_output_path(self): if self.options.output_file: - output_path = os.path.join(os.path.expanduser(os.path.expandvars(self.options.path)), self.options.output_file) + # This is helpful for folks that run the embroider extension + # manually from the command line (without Inkscape) for + # debugging purposes. + output_path = os.path.join(os.path.expanduser(os.path.expandvars(self.options.path.decode("UTF-8"))), + self.options.output_file.decode("UTF-8")) else: csv_filename = '%s.%s' % (self.get_base_file_name(), self.options.output_format) - output_path = os.path.join(self.options.path, csv_filename) + output_path = os.path.join(self.options.path.decode("UTF-8"), csv_filename) def add_suffix(path, suffix): if suffix > 0: diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 4d04ba230..a3ba77848 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -138,8 +138,11 @@ class ParamsTab(ScrolledPanel): if self.toggle: checked = self.enabled() - if self.toggle_checkbox in self.changed_inputs and not self.toggle.inverse: - values[self.toggle.name] = checked + if self.toggle_checkbox in self.changed_inputs: + if self.toggle.inverse: + values[self.toggle.name] = not checked + else: + values[self.toggle.name] = checked if not checked: # Ignore params on this tab if the toggle is unchecked, diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 9d946ae2e..8c8cdefd1 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -312,6 +312,15 @@ def travel_grating(shape, angle, row_spacing): return shgeo.MultiLineString(segments) +def ensure_multi_line_string(thing): + """Given either a MultiLineString or a single LineString, return a MultiLineString""" + + if isinstance(thing, shgeo.LineString): + return shgeo.MultiLineString([thing]) + else: + return thing + + def build_travel_edges(shape, fill_angle): r"""Given a graph, compute the interior travel edges. @@ -359,10 +368,10 @@ def build_travel_edges(shape, fill_angle): for ls in mls for coord in ls.coords] - diagonal_edges = grating1.symmetric_difference(grating2) + diagonal_edges = ensure_multi_line_string(grating1.symmetric_difference(grating2)) # without this, floating point inaccuracies prevent the intersection points from lining up perfectly. - vertical_edges = snap(grating3.difference(grating1), diagonal_edges, 0.005) + vertical_edges = ensure_multi_line_string(snap(grating3.difference(grating1), diagonal_edges, 0.005)) return endpoints, chain(diagonal_edges, vertical_edges) diff --git a/lib/svg/units.py b/lib/svg/units.py index 0de410ab9..739dcbb49 100644 --- a/lib/svg/units.py +++ b/lib/svg/units.py @@ -1,7 +1,8 @@ import simpletransform -from ..utils import cache from ..i18n import _ +from ..utils import cache + # modern versions of Inkscape use 96 pixels per inch as per the CSS standard PIXELS_PER_MM = 96 / 25.4 @@ -91,6 +92,15 @@ def get_doc_size(svg): width = svg.get('width') height = svg.get('height') + if width == "100%" and height == "100%": + # Some SVG editors set width and height to "100%". I can't find any + # solid documentation on how one is supposed to interpret that, so + # just ignore it and use the viewBox. That seems to have the intended + # result anyway. + + width = None + height = None + if width is None or height is None: # fall back to the dimensions from the viewBox viewbox = get_viewbox(svg)