diff options
| author | George Steel <george.steel@gmail.com> | 2022-12-10 19:27:20 -0500 |
|---|---|---|
| committer | George Steel <george.steel@gmail.com> | 2022-12-10 19:29:01 -0500 |
| commit | 2cec72cbbd73b2dece5d15715e2ab8bddd417c69 (patch) | |
| tree | aff4569cb68f626e05489a86511fbd4be8325e17 /lib | |
| parent | 495a22ea55234e032ac526450fd7969d9aa799c8 (diff) | |
| parent | e52f889bb0cb208685b3f3ef8271f63ecd318af1 (diff) | |
Merge branch 'george-steel/expose-trim-after'
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/elements/element.py | 12 | ||||
| -rw-r--r-- | lib/elements/empty_d_object.py | 4 | ||||
| -rw-r--r-- | lib/elements/fill_stitch.py | 21 | ||||
| -rw-r--r-- | lib/elements/satin_column.py | 111 | ||||
| -rw-r--r-- | lib/elements/utils.py | 30 | ||||
| -rw-r--r-- | lib/extensions/__init__.py | 3 | ||||
| -rw-r--r-- | lib/extensions/convert_to_satin.py | 10 | ||||
| -rw-r--r-- | lib/extensions/lettering.py | 4 | ||||
| -rw-r--r-- | lib/extensions/lettering_along_path.py | 144 | ||||
| -rw-r--r-- | lib/lettering/font.py | 3 | ||||
| -rw-r--r-- | lib/stitches/auto_satin.py | 4 | ||||
| -rw-r--r-- | lib/stitches/fill.py | 3 | ||||
| -rw-r--r-- | lib/stitches/guided_fill.py | 2 | ||||
| -rw-r--r-- | lib/stitches/ripple_stitch.py | 2 | ||||
| -rw-r--r-- | lib/threads/color.py | 3 | ||||
| -rw-r--r-- | lib/utils/geometry.py | 41 |
16 files changed, 271 insertions, 126 deletions
diff --git a/lib/elements/element.py b/lib/elements/element.py index 96949ca3..84141d4f 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -342,10 +342,22 @@ class EmbroideryElement(object): return self.strip_control_points(path[0]) @property + @param('trim_after', + _ ('Trim After'), + tooltip=_('Add a TRIM command after stitching this object.'), + type='boolean', + default=False, + sort_index=52) def trim_after(self): return self.get_boolean_param('trim_after', False) @property + @param('stop_after', + _ ('Stop After'), + tooltip=_('Add a STOP command after stitching this object.'), + type='boolean', + default=False, + sort_index=53) def stop_after(self): return self.get_boolean_param('stop_after', False) diff --git a/lib/elements/empty_d_object.py b/lib/elements/empty_d_object.py index 8f3b08de..6f010488 100644 --- a/lib/elements/empty_d_object.py +++ b/lib/elements/empty_d_object.py @@ -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...') ] diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index a5248952..f4a21f90 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -113,8 +113,8 @@ class FillStitch(EmbroideryElement): @property @param('guided_fill_strategy', _('Guided Fill Strategy'), type='dropdown', default=0, options=[_("Copy"), _("Parallel Offset")], select_items=[('fill_method', 2)], sort_index=3, - tooltip=_('Copy (the default) will fill the shape with shifted copies of the line.' + - 'Parallel offset will ensure that each line is always a consistent distance from its neighbor.' + + tooltip=_('Copy (the default) will fill the shape with shifted copies of the line. ' + 'Parallel offset will ensure that each line is always a consistent distance from its neighbor. ' 'Sharp corners may be introduced.')) def guided_fill_strategy(self): return self.get_int_param('guided_fill_strategy', 0) @@ -216,7 +216,7 @@ class FillStitch(EmbroideryElement): @property @param('staggers', _('Stagger rows this many times before repeating'), - tooltip=_('Length of the cycle by which successive stitch rows are staggered.' + tooltip=_('Length of the cycle by which successive stitch rows are staggered. ' 'Fractional values are allowed and can have less visible diagonals than integer values.'), type='int', sort_index=6, @@ -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) diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 3a183ab0..0850d313 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -15,23 +15,11 @@ from shapely.ops import nearest_points 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).") @@ -185,7 +173,7 @@ class SatinColumn(EmbroideryElement): @param( 'pull_compensation_percent', _('Pull compensation percentage'), - tooltip=_('Additional pull compensation which varries as a percentage of stitch width. ' + tooltip=_('Additional pull compensation which varies as a percentage of stitch width. ' 'Two values separated by a space may be used for an aysmmetric effect.'), unit='% (each side)', type='float', @@ -390,24 +378,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 @@ -431,21 +401,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 @@ -498,39 +465,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)): @@ -554,19 +504,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) @@ -574,7 +520,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): @@ -732,14 +678,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): @@ -781,7 +723,7 @@ class SatinColumn(EmbroideryElement): # don't contract beyond the midpoint, or we'll start expanding if offset_total < -distance: - scale = distance / offset_total + scale = -distance / offset_total offset_a = offset_a * scale offset_b = offset_b * scale @@ -931,6 +873,7 @@ class SatinColumn(EmbroideryElement): # center line between the bezier curves. inset_prop = -np.array([self.center_walk_underlay_position, 100-self.center_walk_underlay_position]) / 100 + # Do it like contour underlay, but inset all the way to the center. forward, back = self.plot_points_on_rails( self.center_walk_underlay_stitch_length, diff --git a/lib/elements/utils.py b/lib/elements/utils.py index dafde759..f7ee8dbc 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -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)] diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index 5c702ce8..a2261a80 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -48,6 +48,8 @@ from .stitch_plan_preview import StitchPlanPreview from .stitch_plan_preview_undo import StitchPlanPreviewUndo from .zip import Zip +from.lettering_along_path import LetteringAlongPath + __all__ = extensions = [StitchPlanPreview, StitchPlanPreviewUndo, DensityMap, @@ -75,6 +77,7 @@ __all__ = extensions = [StitchPlanPreview, LetteringRemoveKerning, LetteringCustomFontDir, LetteringForceLockStitches, + LetteringAlongPath, LettersToFont, Troubleshoot, RemoveEmbroiderySettings, diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py index 93850789..5512a095 100644 --- a/lib/extensions/convert_to_satin.py +++ b/lib/extensions/convert_to_satin.py @@ -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)) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 40fd48af..0d18449a 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -71,8 +71,8 @@ class LetteringFrame(wx.Frame): self.back_and_forth_checkbox = wx.CheckBox(self, label=_("Stitch lines of text back and forth")) self.back_and_forth_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("back_and_forth", event)) - self.trim_option_choice = wx.Choice(self, choices=["Never", "after each line", "after each word", "after each letter"], - name=_("Add trim after")) + self.trim_option_choice = wx.Choice(self, choices=[_("Never"), _("after each line"), _("after each word"), _("after each letter")], + name=_("Add trim command")) self.trim_option_choice.Bind(wx.EVT_CHOICE, lambda event: self.on_trim_option_change(event)) # text editor diff --git a/lib/extensions/lettering_along_path.py b/lib/extensions/lettering_along_path.py new file mode 100644 index 00000000..63ffc817 --- /dev/null +++ b/lib/extensions/lettering_along_path.py @@ -0,0 +1,144 @@ +# Authors: see git history +# +# Copyright (c) 2021 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import json +import sys +from math import atan2, degrees + +from inkex import Boolean, Transform, errormsg +from shapely.ops import substring + +from ..elements import Stroke +from ..i18n import _ +from ..svg import get_correction_transform +from ..svg.tags import EMBROIDERABLE_TAGS, INKSTITCH_LETTERING, SVG_GROUP_TAG +from ..utils import DotDict +from ..utils import Point as InkstitchPoint +from .base import InkstitchExtension + + +class LetteringAlongPath(InkstitchExtension): + ''' + This extension aligns an Ink/Stitch Lettering group along a path + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-o", "--options", type=str, default=None, dest="page_1") + self.arg_parser.add_argument("-i", "--info", type=str, default=None, dest="page_2") + self.arg_parser.add_argument("-s", "--stretch-spaces", type=Boolean, default=False, dest="stretch_spaces") + + def effect(self): + # we ignore everything but the first path/text + text, path = self.get_selection() + self.load_settings(text) + + glyphs = [glyph for glyph in text.iterdescendants(SVG_GROUP_TAG) if len(glyph.label) == 1] + if not glyphs: + errormsg(_("The text doesn't contain any glyphs.")) + sys.exit(1) + + path = Stroke(path).as_multi_line_string().geoms[0] + path_length = path.length + + # overall bounding box - get from direct glyph parent + text_bbox = glyphs[0].getparent().bounding_box() + text_y = text_bbox.bottom + + if self.options.stretch_spaces: + text_content = self.settings["text"] + space_indices = [i for i, t in enumerate(text_content) if t == " "] + text_width = text_bbox.width + + if len(text_content) - 1 != 0: + stretch_space = (path_length - text_width) / (len(text_content) - 1) + else: + stretch_space = 0 + else: + stretch_space = 0 + space_indices = [] + + self.transform_glyphs(glyphs, path, stretch_space, space_indices, text_y) + + def transform_glyphs(self, glyphs, path, stretch_space, space_indices, text_y): + text_scale = Transform(f'scale({self.settings["scale"] / 100})') + distance = 0 + old_bbox = None + i = 0 + + for glyph in glyphs: + # dimensions + bbox = glyph.bounding_box() + left = bbox.left + width = bbox.width + + # adjust position + if old_bbox: + right = old_bbox.right + distance += left - right + stretch_space + + if self.options.stretch_spaces and i in space_indices: + distance += stretch_space + i += 1 + + new_distance = distance + width + + # calculate and apply transform + first = substring(path, distance, distance) + last = substring(path, new_distance, new_distance) + + angle = degrees(atan2(last.y - first.y, last.x - first.x)) % 360 + translate = InkstitchPoint(first.x, first.y) - InkstitchPoint(left, text_y) + + transform = Transform(f"rotate({angle}, {first.x}, {first.y}) translate({translate.x} {translate.y})") + correction_transform = Transform(get_correction_transform(glyph)) + glyph.transform = correction_transform @ transform @ glyph.transform @ text_scale + + # set values for next iteration + distance = new_distance + old_bbox = bbox + i += 1 + + def load_settings(self, text): + """Load the settings saved into the text element""" + + self.settings = DotDict({ + "text": "", + "back_and_forth": False, + "font": None, + "scale": 100, + "trim_option": 0 + }) + + if INKSTITCH_LETTERING in text.attrib: + try: + self.settings.update(json.loads(text.get(INKSTITCH_LETTERING))) + except (TypeError, ValueError): + pass + + def get_selection(self): + groups = list() + paths = list() + + for node in self.svg.selection: + lettering = False + if node.tag == SVG_GROUP_TAG and INKSTITCH_LETTERING in node.attrib: + groups.append(node) + lettering = True + continue + + for group in node.iterancestors(SVG_GROUP_TAG): + if INKSTITCH_LETTERING in group.attrib: + groups.append(group) + lettering = True + break + + if not lettering and node.tag in EMBROIDERABLE_TAGS: + paths.append(node) + + if not groups or not paths: + errormsg(_("Please select one path and one Ink/Stitch lettering group.")) + sys.exit(1) + + return [groups[0], paths[0]] diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 5a617da1..6960105f 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -11,7 +11,7 @@ from random import randint import inkex from ..commands import add_commands, ensure_symbol -from ..elements import FillStitch, Stroke, nodes_to_elements +from ..elements import FillStitch, SatinColumn, Stroke, nodes_to_elements from ..exceptions import InkstitchException from ..extensions.lettering_custom_font_dir import get_custom_font_dir from ..i18n import _, get_languages @@ -417,6 +417,7 @@ class Font(object): """ elements = nodes_to_elements(group.iterdescendants(SVG_PATH_TAG)) + elements = [element for element in elements if isinstance(element, SatinColumn) or isinstance(element, Stroke)] if elements: auto_satin(elements, preserve_order=True, trim=False) diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py index 93bffd57..6d792b4e 100644 --- a/lib/stitches/auto_satin.py +++ b/lib/stitches/auto_satin.py @@ -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) diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 60a0cb7d..11c9259b 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -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 diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 14c58b7a..a9906bfb 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -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): diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py index 67362f12..6b7ce6ca 100644 --- a/lib/stitches/ripple_stitch.py +++ b/lib/stitches/ripple_stitch.py @@ -220,7 +220,7 @@ def _get_start_rotation(line): def _generate_satin_guide_helper_lines(stroke, outline, guide_line): spacing = guide_line.center_line.length / (stroke.get_line_count() - 1) - rail_points = guide_line.plot_points_on_rails(spacing, 0) + rail_points = guide_line.plot_points_on_rails(spacing) point0 = rail_points[0][0] point1 = rail_points[1][0] diff --git a/lib/threads/color.py b/lib/threads/color.py index 85c8948c..458752da 100644 --- a/lib/threads/color.py +++ b/lib/threads/color.py @@ -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) diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py index 30457749..98f40709 100644 --- a/lib/utils/geometry.py +++ b/lib/utils/geometry.py @@ -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 |
