diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/commands.py | 8 | ||||
| -rw-r--r-- | lib/elements/clone.py | 81 | ||||
| -rw-r--r-- | lib/elements/element.py | 27 | ||||
| -rw-r--r-- | lib/elements/fill_stitch.py | 3 | ||||
| -rw-r--r-- | lib/elements/gradient_fill.py | 79 | ||||
| -rw-r--r-- | lib/elements/satin_column.py | 195 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 23 | ||||
| -rw-r--r-- | lib/extensions/__init__.py | 2 | ||||
| -rw-r--r-- | lib/extensions/gradient_blocks.py | 69 | ||||
| -rw-r--r-- | lib/extensions/lettering.py | 31 | ||||
| -rw-r--r-- | lib/lettering/font.py | 80 | ||||
| -rw-r--r-- | lib/lettering/font_variant.py | 5 | ||||
| -rw-r--r-- | lib/stitches/fill.py | 2 | ||||
| -rw-r--r-- | lib/stitches/guided_fill.py | 2 | ||||
| -rw-r--r-- | lib/stitches/ripple_stitch.py | 2 | ||||
| -rw-r--r-- | lib/svg/tags.py | 6 |
16 files changed, 486 insertions, 129 deletions
diff --git a/lib/commands.py b/lib/commands.py index a7affb6d..6280fc0c 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -347,7 +347,13 @@ def get_command_pos(element, index, total): # Put command symbols 30 pixels out from the shape, spaced evenly around it. # get a line running 30 pixels out from the shape - outline = element.shape.buffer(30).exterior + + if not isinstance(element.shape.buffer(30), shgeo.MultiPolygon): + outline = element.shape.buffer(30).exterior + else: + polygons = element.shape.buffer(30).geoms + polygon = polygons[len(polygons)-1] + outline = polygon.exterior # find the top center point on the outline and start there top_center = shgeo.Point(outline.centroid.x, outline.bounds[1]) diff --git a/lib/elements/clone.py b/lib/elements/clone.py index d9185012..b5507e4f 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -3,12 +3,13 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -from math import atan, degrees +from math import atan2, degrees, radians + +from inkex import CubicSuperPath, Path, Transform from ..commands import is_command_symbol from ..i18n import _ from ..svg.path import get_node_transform -from ..svg.svg import find_elements from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_USE_TAG, XLINK_HREF) from ..utils import cache @@ -60,7 +61,16 @@ class Clone(EmbroideryElement): type='float') @cache def clone_fill_angle(self): - return self.get_float_param('angle', 0) + return self.get_float_param('angle') or None + + @property + @param('flip_angle', + _('Flip angle'), + tooltip=_("Flip automatically calucalted angle if it appears to be wrong."), + type='boolean') + @cache + def flip_angle(self): + return self.get_boolean_param('flip_angle') def clone_to_element(self, node): from .utils import node_to_elements @@ -73,33 +83,51 @@ class Clone(EmbroideryElement): if source_node.tag not in EMBROIDERABLE_TAGS: return [] - self.node.style = source_node.specified_style() - - # a. a custom set fill angle - # b. calculated rotation for the cloned fill element to look exactly as it's source - param = INKSTITCH_ATTRIBS['angle'] - if self.clone_fill_angle is not None: - angle = self.clone_fill_angle + old_transform = source_node.get('transform', '') + source_transform = source_node.composed_transform() + source_path = Path(source_node.get_path()).transform(source_transform) + transform = Transform(source_node.get('transform', '')) @ -source_transform + transform @= self.node.composed_transform() @ Transform(source_node.get('transform', '')) + source_node.set('transform', transform) + + old_angle = float(source_node.get(INKSTITCH_ATTRIBS['angle'], 0)) + if self.clone_fill_angle is None: + rot = transform.add_rotate(-old_angle) + angle = self._get_rotation(rot, source_node, source_path) + if angle is not None: + source_node.set(INKSTITCH_ATTRIBS['angle'], angle) else: - # clone angle - clone_mat = self.node.composed_transform() - clone_angle = degrees(atan(-clone_mat[1][0]/clone_mat[1][1])) - # source node angle - source_mat = source_node.composed_transform() - source_angle = degrees(atan(-source_mat[1][0]/source_mat[1][1])) - # source node fill angle - source_fill_angle = source_node.get(param, 0) - - angle = clone_angle + float(source_fill_angle) - source_angle - self.node.set(param, str(angle)) - - elements = self.clone_to_element(self.node) + source_node.set(INKSTITCH_ATTRIBS['angle'], self.clone_fill_angle) + elements = self.clone_to_element(source_node) for element in elements: - patches.extend(element.to_stitch_groups(last_patch)) + stitch_groups = element.to_stitch_groups(last_patch) + patches.extend(stitch_groups) + source_node.set('transform', old_transform) + source_node.set(INKSTITCH_ATTRIBS['angle'], old_angle) return patches + def _get_rotation(self, transform, source_node, source_path): + try: + rotation = transform.rotation_degrees() + except ValueError: + source_path = CubicSuperPath(source_path)[0] + clone_path = Path(source_node.get_path()).transform(source_node.composed_transform()) + clone_path = CubicSuperPath(clone_path)[0] + + angle_source = atan2(source_path[1][1][1] - source_path[0][1][1], source_path[1][1][0] - source_path[0][1][0]) + angle_clone = atan2(clone_path[1][1][1] - clone_path[0][1][1], clone_path[1][1][0] - clone_path[0][1][0]) + angle_embroidery = radians(-float(source_node.get(INKSTITCH_ATTRIBS['angle'], 0))) + + diff = angle_source - angle_embroidery + rotation = degrees(diff + angle_clone) + + if self.flip_angle: + rotation = -degrees(diff - angle_clone) + + return -rotation + def get_clone_style(self, style_name, node, default=None): style = node.style[style_name] or default return style @@ -132,7 +160,4 @@ def is_embroiderable_clone(node): def get_clone_source(node): - source_id = node.get(XLINK_HREF)[1:] - xpath = ".//*[@id='%s']" % (source_id) - source_node = find_elements(node, xpath)[0] - return source_node + return node.href diff --git a/lib/elements/element.py b/lib/elements/element.py index 75d22580..96949ca3 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -5,6 +5,7 @@ import sys from copy import deepcopy +import numpy as np import inkex from inkex import bezier @@ -135,6 +136,32 @@ class EmbroideryElement(object): return value + # returns 2 float values as a numpy array + # if a single number is given in the param, it will apply to both returned values. + # Not cached the cache will crash if the default is a numpy array. + # The ppoperty calling this will need to cache itself and can safely do so since it has no parameters + def get_split_float_param(self, param, default=(0, 0)): + default = np.array(default) # type coersion in case the default is a tuple + + raw = self.get_param(param, "") + parts = raw.split() + try: + if len(parts) == 0: + return default + elif len(parts) == 1: + a = float(parts[0]) + return np.array([a, a]) + else: + a = float(parts[0]) + b = float(parts[1]) + return np.array([a, b]) + except (TypeError, ValueError): + return default + + # not cached + def get_split_mm_param_as_px(self, param, default): + return self.get_split_float_param(param, default) * PIXELS_PER_MM + def set_param(self, name, value): param = INKSTITCH_ATTRIBS[name] self.node.set(param, str(value)) diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index ca44e3cc..a5248952 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -216,7 +216,8 @@ 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. Fractional values are allowed and can have less visible diagonals than integer values.'), + 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, select_items=[('fill_method', 0), ('fill_method', 2), ('fill_method', 3)], diff --git a/lib/elements/gradient_fill.py b/lib/elements/gradient_fill.py new file mode 100644 index 00000000..5ac49c4e --- /dev/null +++ b/lib/elements/gradient_fill.py @@ -0,0 +1,79 @@ +from math import pi + +from inkex import DirectedLineSegment, Transform +from shapely import geometry as shgeo +from shapely.affinity import affine_transform, rotate +from shapely.ops import split + +from ..svg import get_correction_transform + + +def gradient_shapes_and_attributes(element, shape): + # e.g. url(#linearGradient872) -> linearGradient872 + color = element.color[5:-1] + xpath = f'.//svg:defs/svg:linearGradient[@id="{color}"]' + gradient = element.node.getroottree().getroot().findone(xpath) + gradient.apply_transform() + point1 = (float(gradient.get('x1')), float(gradient.get('y1'))) + point2 = (float(gradient.get('x2')), float(gradient.get('y2'))) + # get 90° angle to calculate the splitting angle + line = DirectedLineSegment(point1, point2) + angle = line.angle - (pi / 2) + # Ink/Stitch somehow turns the stitch angle + stitch_angle = angle * -1 + # create bbox polygon to calculate the length necessary to make sure that + # the gradient splitter lines will cut the entire design + bbox = element.node.bounding_box() + bbox_polygon = shgeo.Polygon([(bbox.left, bbox.top), (bbox.right, bbox.top), + (bbox.right, bbox.bottom), (bbox.left, bbox.bottom)]) + # gradient stops + offsets = gradient.stop_offsets + stop_styles = gradient.stop_styles + # now split the shape according to the gradient stops + polygons = [] + colors = [] + attributes = [] + previous_color = None + end_row_spacing = None + for i, offset in enumerate(offsets): + shape_rest = [] + split_point = shgeo.Point(line.point_at_ratio(float(offset))) + length = split_point.hausdorff_distance(bbox_polygon) + split_line = shgeo.LineString([(split_point.x - length - 2, split_point.y), + (split_point.x + length + 2, split_point.y)]) + split_line = rotate(split_line, angle, origin=split_point, use_radians=True) + transform = -Transform(get_correction_transform(element.node)) + transform = list(transform.to_hexad()) + split_line = affine_transform(split_line, transform) + offset_line = split_line.parallel_offset(1, 'right') + polygon = split(shape, split_line) + color = stop_styles[i]['stop-color'] + # does this gradient line split the shape + offset_outside_shape = len(polygon.geoms) == 1 + for poly in polygon.geoms: + if isinstance(poly, shgeo.Polygon) and element.shape_is_valid(poly): + if poly.intersects(offset_line): + if previous_color: + polygons.append(poly) + colors.append(previous_color) + attributes.append({'angle': stitch_angle, 'end_row_spacing': end_row_spacing, 'color': previous_color}) + polygons.append(poly) + attributes.append({'angle': stitch_angle + pi, 'end_row_spacing': end_row_spacing, 'color': color}) + else: + shape_rest.append(poly) + shape = shgeo.MultiPolygon(shape_rest) + previous_color = color + end_row_spacing = element.row_spacing * 2 + # add left over shape(s) + if shape: + if offset_outside_shape: + for s in shape.geoms: + polygons.append(s) + attributes.append({'color': stop_styles[-2]['stop-color'], 'angle': stitch_angle, 'end_row_spacing': end_row_spacing}) + stitch_angle += pi + else: + end_row_spacing = None + for s in shape.geoms: + polygons.append(s) + attributes.append({'color': stop_styles[-1]['stop-color'], 'angle': stitch_angle, 'end_row_spacing': end_row_spacing}) + return polygons, attributes diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 51ed43a4..3a183ab0 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -5,6 +5,7 @@ import random from copy import deepcopy from itertools import chain +import numpy as np from inkex import paths from shapely import affinity as shaffinity @@ -15,7 +16,7 @@ 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 .element import EmbroideryElement, param +from .element import EmbroideryElement, param, PIXELS_PER_MM from .validation import ValidationError, ValidationWarning @@ -163,8 +164,8 @@ class SatinColumn(EmbroideryElement): @property @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), - tooltip=_('Peak-to-peak distance between zig-zags.'), - unit='mm', + tooltip=_('Peak-to-peak distance between zig-zags. This is double the mm/stitch measurement used by most mechanical machines.'), + unit='mm/cycle', type='float', default=0.4) def zigzag_spacing(self): @@ -184,40 +185,43 @@ class SatinColumn(EmbroideryElement): @param( 'pull_compensation_percent', _('Pull compensation percentage'), - tooltip=_('pull compensation in percentage'), - unit='%', - type='int', + tooltip=_('Additional pull compensation which varries as a percentage of stitch width. ' + 'Two values separated by a space may be used for an aysmmetric effect.'), + unit='% (each side)', + type='float', default=0) + @cache def pull_compensation_percent(self): # pull compensation as a percentage of the width - return max(self.get_int_param("pull_compensation_percent", 0), 0) + return self.get_split_float_param("pull_compensation_percent", (0, 0)) @property @param( 'pull_compensation_mm', _('Pull compensation'), - tooltip=_('Satin stitches pull the fabric together, resulting in a column narrower than you draw in Inkscape. ' - 'This setting expands each pair of needle penetrations outward from the center of the satin column.'), - unit='mm', + tooltip=_('Satin stitches pull the fabric together, resulting in a column narrower than you draw in Inkscape. ' + 'This setting expands each pair of needle penetrations outward from the center of the satin column by a fixed length. ' + 'Two values separated by a space may be used for an aysmmetric effect.'), + unit='mm (each side)', type='float', default=0) - def pull_compensation(self): + @cache + def pull_compensation_px(self): # In satin stitch, the stitches have a tendency to pull together and # narrow the entire column. We can compensate for this by stitching # wider than we desire the column to end up. - return self.get_float_param("pull_compensation_mm", 0) + return self.get_split_mm_param_as_px("pull_compensation_mm", (0, 0)) @property @param( - 'pull_compensation_rails', - _('Apply pull compensation to '), - tooltip=_('decide wether the pull compensations should be applied to both side or only to a given one'), - type='dropdown', - options=[_("Both rails"), _("First rail only"), _("Second rail only")], - default=0) - def pull_compensation_rails(self): - # 0=Both | 1 = First Rail | 2 = Second Rail - return self.get_int_param("pull_compensation_rails", 0) + 'swap_satin_rails', + _('Swap rails'), + tooltip=_('Swaps the first and second rails of the satin column, ' + 'affecting which side the thread finished on as well as any sided properties'), + type='boolean', + default='false') + def swap_rails(self): + return self.get_boolean_param('swap_satin_rails', False) @property @param('contour_underlay', _('Contour underlay'), type='toggle', group=_('Contour Underlay')) @@ -233,15 +237,28 @@ class SatinColumn(EmbroideryElement): @property @param('contour_underlay_inset_mm', - _('Contour underlay inset amount'), - tooltip=_('Shrink the outline, to prevent the underlay from showing around the outside of the satin column.'), - unit='mm', + _('Inset distance (fixed)'), + tooltip=_('Shrink the outline by a fixed length, to prevent the underlay from showing around the outside of the satin column.'), group=_('Contour Underlay'), - type='float', - default=0.4) - def contour_underlay_inset(self): + unit='mm (each side)', type='float', default=0.4, + sort_index=2) + @cache + def contour_underlay_inset_px(self): + # how far inside the edge of the column to stitch the underlay + return self.get_split_mm_param_as_px("contour_underlay_inset_mm", (0.4, 0.4)) + + @property + @param('contour_underlay_inset_percent', + _('Inset distance (proportional)'), + tooltip=_('Shrink the outline by a proportion of the column width, ' + 'to prevent the underlay from showing around the outside of the satin column.'), + group=_('Contour Underlay'), + unit='% (each side)', type='float', default=0, + sort_index=3) + @cache + def contour_underlay_inset_percent(self): # how far inside the edge of the column to stitch the underlay - return self.get_float_param("contour_underlay_inset_mm", 0.4) + return self.get_split_float_param("contour_underlay_inset_percent", (0, 0)) @property @param('center_walk_underlay', _('Center-walk underlay'), type='toggle', group=_('Center-Walk Underlay')) @@ -256,11 +273,27 @@ class SatinColumn(EmbroideryElement): return max(self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5), 0.01) @property - @param('center_walk_underlay_repeats', _('Repeats'), group=_('Center-Walk Underlay'), type='int', default=2, sort_index=2) + @param('center_walk_underlay_repeats', + _('Repeats'), + tooltip=_('For an odd number of repeats, this will reverse the direction the satin column is stitched, ' + 'causing stitching to both begin and end at the start point.'), + group=_('Center-Walk Underlay'), + type='int', default=2, + sort_index=2) def center_walk_underlay_repeats(self): return max(self.get_int_param("center_walk_underlay_repeats", 2), 1) @property + @param('center_walk_underlay_position', + _('Position'), + tooltip=_('Position of underlay from between the rails. 0% is along the first rail, 50% is centered, 100% is along the second rail.'), + group=_('Center-Walk Underlay'), + type='float', unit='%', default=50, + sort_index=3) + def center_walk_underlay_position(self): + return min(100, max(0, self.get_float_param("center_walk_underlay_position", 50))) + + @property @param('zigzag_underlay', _('Zig-zag underlay'), type='toggle', group=_('Zig-zag Underlay')) def zigzag_underlay(self): return self.get_boolean_param("zigzag_underlay") @@ -278,13 +311,13 @@ class SatinColumn(EmbroideryElement): @property @param('zigzag_underlay_inset_mm', - _('Inset amount'), + _('Inset amount (fixed)'), tooltip=_('default: half of contour underlay inset'), - unit='mm', + unit='mm (each side)', group=_('Zig-zag Underlay'), type='float', default="") - def zigzag_underlay_inset(self): + def zigzag_underlay_inset_px(self): # how far in from the edge of the satin the points in the zigzags # should be @@ -292,7 +325,22 @@ class SatinColumn(EmbroideryElement): # doing both contour underlay and zigzag underlay, make sure the # points of the zigzag fall outside the contour underlay but inside # the edges of the satin column. - return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0 + default = self.contour_underlay_inset_px * 0.5 / PIXELS_PER_MM + x = self.get_split_mm_param_as_px("zigzag_underlay_inset_mm", default) + return x + + @property + @param('zigzag_underlay_inset_percent', + _('Inset amount (proportional)'), + tooltip=_('default: half of contour underlay inset'), + unit='% (each side)', + group=_('Zig-zag Underlay'), + type='float', + default="") + @cache + def zigzag_underlay_inset_percent(self): + default = self.contour_underlay_inset_percent * 0.5 + return self.get_split_float_param("zigzag_underlay_inset_percent", default) @property @param('zigzag_underlay_max_stitch_length_mm', @@ -326,7 +374,11 @@ class SatinColumn(EmbroideryElement): @cache def rails(self): """The rails in order, as point lists""" - return [subpath for i, subpath in enumerate(self.csp) if i in self.rail_indices] + rails = [subpath for i, subpath in enumerate(self.csp) if i in self.rail_indices] + if len(rails) == 2 and self.swap_rails: + return [rails[1], rails[0]] + else: + return rails @property @cache @@ -390,7 +442,7 @@ class SatinColumn(EmbroideryElement): # intersect with the rails even with floating point inaccuracy. start = Point(*start) end = Point(*end) - start, end = self.offset_points(start, end, 0.01) + start, end = self.offset_points(start, end, (0.01, 0.01), (0, 0)) start = list(start) end = list(end) @@ -616,7 +668,7 @@ class SatinColumn(EmbroideryElement): """ # like in do_satin() - points = list(chain.from_iterable(zip(*self.plot_points_on_rails(self.zigzag_spacing, 0)))) + points = list(chain.from_iterable(zip(*self.plot_points_on_rails(self.zigzag_spacing)))) if isinstance(split_point, float): index_of_closest_stitch = int(round(len(points) * split_point)) @@ -708,46 +760,35 @@ class SatinColumn(EmbroideryElement): @cache def center_line(self): # similar technique to do_center_walk() - center_walk, _ = self.plot_points_on_rails(self.zigzag_spacing, -100000) + center_walk, _ = self.plot_points_on_rails(self.zigzag_spacing, (0, 0), (-0.5, -0.5)) return shgeo.LineString(center_walk) - def offset_points(self, pos1, pos2, offset, offset_percent=0, offset_rails=0): + def offset_points(self, pos1, pos2, offset_px, offset_proportional): # Expand or contract two points about their midpoint. This is # useful for pull compensation and insetting underlay. distance = (pos1 - pos2).length() - offset_px = 0 - if offset: - offset_px += offset - if offset_percent: - offset_px += ((offset_percent / 100) * distance) if distance < 0.0001: # if they're the same point, we don't know which direction # to offset in, so we have to just return the points return pos1, pos2 - # don't contract beyond the midpoint, or we'll start expanding - if offset_px < -distance / 2.0: - offset_px = -distance / 2.0 - - # chose how to apply on the rails - - coeff1 = 1 - coeff2 = 1 + # calculate the offset for each side + offset_a = offset_px[0] + (distance * offset_proportional[0]) + offset_b = offset_px[1] + (distance * offset_proportional[1]) + offset_total = offset_a + offset_b - if offset_rails == 1: - coeff1 = 2 - coeff2 = 0 - - if offset_rails == 2: - coeff1 = 0 - coeff2 = 2 + # don't contract beyond the midpoint, or we'll start expanding + if offset_total < -distance: + scale = distance / offset_total + offset_a = offset_a * scale + offset_b = offset_b * scale - pos1 = pos1 + (pos1 - pos2).unit() * offset_px * coeff1 - pos2 = pos2 + (pos2 - pos1).unit() * offset_px * coeff2 + out1 = pos1 + (pos1 - pos2).unit() * offset_a + out2 = pos2 + (pos2 - pos1).unit() * offset_b - return pos1, pos2 + return out1, out2 def walk(self, path, start_pos, start_index, distance): # Move <distance> pixels along <path>, which is a sequence of line @@ -782,13 +823,13 @@ class SatinColumn(EmbroideryElement): distance_remaining -= segment_length pos = segment_end - def plot_points_on_rails(self, spacing, offset, offset_percent=0, offset_rails=0): + def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_proportional=(0, 0)): # Take a section from each rail in turn, and plot out an equal number # of points on both rails. Return the points plotted. The points will # be contracted or expanded by offset using self.offset_points(). def add_pair(pos0, pos1): - pos0, pos1 = self.offset_points(pos0, pos1, offset, offset_percent, offset_rails) + pos0, pos1 = self.offset_points(pos0, pos1, offset_px, offset_proportional) points[0].append(pos0) points[1].append(pos1) @@ -873,7 +914,9 @@ class SatinColumn(EmbroideryElement): def do_contour_underlay(self): # "contour walk" underlay: do stitches up one side and down the # other. - forward, back = self.plot_points_on_rails(self.contour_underlay_stitch_length, -self.contour_underlay_inset) + forward, back = self.plot_points_on_rails( + self.contour_underlay_stitch_length, + -self.contour_underlay_inset_px, -self.contour_underlay_inset_percent/100) stitches = (forward + list(reversed(back))) if self._center_walk_is_odd(): stitches = (list(reversed(back)) + forward) @@ -887,8 +930,11 @@ class SatinColumn(EmbroideryElement): # Center walk underlay is just a running stitch down and back on the # 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, -100000) + forward, back = self.plot_points_on_rails( + self.center_walk_underlay_stitch_length, + (0, 0), inset_prop) stitches = [] for i in range(self.center_walk_underlay_repeats): @@ -915,7 +961,9 @@ class SatinColumn(EmbroideryElement): patch = StitchGroup(color=self.color) - sides = self.plot_points_on_rails(self.zigzag_underlay_spacing / 2.0, -self.zigzag_underlay_inset) + sides = self.plot_points_on_rails(self.zigzag_underlay_spacing / 2.0, + -self.zigzag_underlay_inset_px, + -self.zigzag_underlay_inset_percent/100) if self._center_walk_is_odd(): sides = [list(reversed(sides[0])), list(reversed(sides[1]))] @@ -950,8 +998,13 @@ class SatinColumn(EmbroideryElement): # print >> dbg, "satin", self.zigzag_spacing, self.pull_compensation patch = StitchGroup(color=self.color) - sides = self.plot_points_on_rails(self.zigzag_spacing, self.pull_compensation, self.pull_compensation_percent, - self.pull_compensation_rails) + + # pull compensation is automatically converted from mm to pixels by get_float_param + sides = self.plot_points_on_rails( + self.zigzag_spacing, + self.pull_compensation_px, + self.pull_compensation_percent/100 + ) if self.max_stitch_length: return self.do_split_stitch(patch, sides) @@ -981,7 +1034,11 @@ class SatinColumn(EmbroideryElement): patch = StitchGroup(color=self.color) - sides = self.plot_points_on_rails(self.zigzag_spacing, self.pull_compensation, self.pull_compensation_percent, self.pull_compensation_rails) + sides = self.plot_points_on_rails( + self.zigzag_spacing, + self.pull_compensation_px, + self.pull_compensation_percent/100 + ) # "left" and "right" here are kind of arbitrary designations meaning # a point from the first and second rail respectively diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index ce973c0e..2854adaf 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -39,6 +39,14 @@ class MultipleGuideLineWarning(ValidationWarning): ] +class SmallZigZagWarning(ValidationWarning): + name = _("Small ZigZag") + description = _("This zig zag stitch has a stroke width smaller than 0.5 units.") + steps_to_solve = [ + _("Set your stroke to be dashed to indicate running stitch. Any kind of dash will work.") + ] + + class Stroke(EmbroideryElement): element_name = _("Stroke") @@ -481,6 +489,15 @@ class Stroke(EmbroideryElement): return guide_lines['satin'][0] return guide_lines['stroke'][0] + def _representative_point(self): + # if we just take the center of a line string we could end up on some point far away from the actual line + try: + coords = list(self.shape.coords) + except NotImplementedError: + # linear rings to not have a coordinate sequence + coords = list(self.shape.exterior.coords) + return coords[int(len(coords)/2)] + def validation_warnings(self): if self.stroke_method == 1 and self.skip_start + self.skip_end >= self.line_count: yield IgnoreSkipValues(self.shape.centroid) @@ -489,4 +506,8 @@ class Stroke(EmbroideryElement): if self.stroke_method == 1: guide_lines = get_marker_elements(self.node, "guide-line", False, True, True) if sum(len(x) for x in guide_lines.values()) > 1: - yield MultipleGuideLineWarning(self.shape.centroid) + yield MultipleGuideLineWarning(self._representative_point()) + + stroke_width, units = parse_length_with_units(self.get_style("stroke-width", "1")) + if not self.dashed and stroke_width <= 0.5: + yield SmallZigZagWarning(self._representative_point()) diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index f1837c59..5c702ce8 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -21,6 +21,7 @@ from .embroider_settings import EmbroiderSettings from .flip import Flip from .generate_palette import GeneratePalette from .global_commands import GlobalCommands +from .gradient_blocks import GradientBlocks from .input import Input from .install import Install from .install_custom_palette import InstallCustomPalette @@ -79,6 +80,7 @@ __all__ = extensions = [StitchPlanPreview, RemoveEmbroiderySettings, Cleanup, BreakApart, + GradientBlocks, ApplyThreadlist, InstallCustomPalette, GeneratePalette, diff --git a/lib/extensions/gradient_blocks.py b/lib/extensions/gradient_blocks.py new file mode 100644 index 00000000..5159149f --- /dev/null +++ b/lib/extensions/gradient_blocks.py @@ -0,0 +1,69 @@ +# Authors: see git history +# +# Copyright (c) 2010 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +from math import degrees + +from inkex import PathElement, errormsg + +from ..elements import FillStitch +from ..elements.gradient_fill import gradient_shapes_and_attributes +from ..i18n import _ +from ..svg import get_correction_transform +from ..svg.tags import INKSTITCH_ATTRIBS +from .base import InkstitchExtension + + +class GradientBlocks(InkstitchExtension): + ''' + This will break apart fill objects with a gradient fill into solid color blocks with end_row_spacing. + ''' + + def effect(self): + if not self.svg.selection: + errormsg(_("Please select at least one object with a gradient fill.")) + return + + if not self.get_elements(): + return + + elements = [element for element in self.elements if (isinstance(element, FillStitch) and self.has_gradient_color(element))] + if not elements: + errormsg(_("Please select at least one object with a gradient fill.")) + return + + for element in elements: + parent = element.node.getparent() + correction_transform = get_correction_transform(element.node) + style = element.node.style + index = parent.index(element.node) + fill_shapes, attributes = gradient_shapes_and_attributes(element, element.shape) + # reverse order so we can always insert with the same index number + fill_shapes.reverse() + attributes.reverse() + for i, shape in enumerate(fill_shapes): + style['fill'] = attributes[i]['color'] + end_row_spacing = attributes[i]['end_row_spacing'] or None + angle = degrees(attributes[i]['angle']) + d = "M " + " ".join([f'{x}, {y}' for x, y in list(shape.exterior.coords)]) + " Z" + block = PathElement(attrib={ + "id": self.uniqueId("path"), + "style": str(style), + "transform": correction_transform, + "d": d, + INKSTITCH_ATTRIBS['angle']: f'{angle: .2f}' + }) + if end_row_spacing: + block.set('inkstitch:end_row_spacing_mm', f'{end_row_spacing: .2f}') + block.set('inkstitch:underpath', False) + parent.insert(index, block) + parent.remove(element.node) + + def has_gradient_color(self, element): + return element.color.startswith('url') and "linearGradient" in element.color + + +if __name__ == '__main__': + e = GradientBlocks() + e.effect() diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index c870a764..40fd48af 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -71,8 +71,9 @@ 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_checkbox = wx.CheckBox(self, label=_("Add trims")) - self.trim_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("trim", 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.Bind(wx.EVT_CHOICE, lambda event: self.on_trim_option_change(event)) # text editor self.text_input_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) @@ -105,7 +106,8 @@ class LetteringFrame(wx.Frame): "text": "", "back_and_forth": False, "font": None, - "scale": 100 + "scale": 100, + "trim_option": 0 }) if INKSTITCH_LETTERING in self.group.attrib: @@ -123,7 +125,7 @@ class LetteringFrame(wx.Frame): def apply_settings(self): """Make the settings in self.settings visible in the UI.""" self.back_and_forth_checkbox.SetValue(bool(self.settings.back_and_forth)) - self.trim_checkbox.SetValue(bool(self.settings.trim)) + self.trim_option_choice.SetSelection(self.settings.trim_option) self.set_initial_font(self.settings.font) self.text_editor.SetValue(self.settings.text) self.scale_spinner.SetValue(self.settings.scale) @@ -219,6 +221,10 @@ class LetteringFrame(wx.Frame): self.settings[attribute] = event.GetEventObject().GetValue() self.preview.update() + def on_trim_option_change(self, event=None): + self.settings.trim_option = self.trim_option_choice.GetCurrentSelection() + self.preview.update() + def on_font_changed(self, event=None): font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) self.settings.font = font.marked_custom_font_id @@ -253,13 +259,6 @@ class LetteringFrame(wx.Frame): self.back_and_forth_checkbox.Disable() self.back_and_forth_checkbox.SetValue(False) - if font.auto_satin: - self.trim_checkbox.Enable() - self.trim_checkbox.SetValue(bool(self.settings.trim)) - else: - self.trim_checkbox.Disable() - self.trim_checkbox.SetValue(False) - self.update_preview() self.Layout() @@ -314,7 +313,9 @@ class LetteringFrame(wx.Frame): font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) try: - font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, + trim_option=self.settings.trim_option) + except FontError as e: if raise_error: inkex.errormsg(_("Error: Text cannot be applied to the document.\n%s") % e) @@ -400,7 +401,11 @@ class LetteringFrame(wx.Frame): # options left_option_sizer = wx.BoxSizer(wx.VERTICAL) left_option_sizer.Add(self.back_and_forth_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 5) - left_option_sizer.Add(self.trim_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) + + trim_option_sizer = wx.BoxSizer(wx.HORIZONTAL) + trim_option_sizer.Add(wx.StaticText(self, wx.ID_ANY, "Add trims"), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 5) + trim_option_sizer.Add(self.trim_option_choice, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) + left_option_sizer.Add(trim_option_sizer, 0, wx.ALIGN_LEFT, 5) font_scale_sizer = wx.BoxSizer(wx.HORIZONTAL) font_scale_sizer.Add(wx.StaticText(self, wx.ID_ANY, "Scale"), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 0) diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 328d2ba4..5a617da1 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -10,15 +10,16 @@ from random import randint import inkex -from ..commands import ensure_symbol -from ..elements import nodes_to_elements +from ..commands import add_commands, ensure_symbol +from ..elements import FillStitch, Stroke, nodes_to_elements from ..exceptions import InkstitchException from ..extensions.lettering_custom_font_dir import get_custom_font_dir from ..i18n import _, get_languages -from ..marker import MARKER, ensure_marker +from ..marker import MARKER, ensure_marker, has_marker from ..stitches.auto_satin import auto_satin -from ..svg.tags import (CONNECTION_END, CONNECTION_START, INKSCAPE_LABEL, - SVG_PATH_TAG, SVG_USE_TAG, XLINK_HREF) +from ..svg.tags import (CONNECTION_END, CONNECTION_START, EMBROIDERABLE_TAGS, + INKSCAPE_LABEL, SVG_GROUP_TAG, SVG_PATH_TAG, + SVG_USE_TAG, XLINK_HREF) from ..utils import Point from .font_variant import FontVariant @@ -180,7 +181,8 @@ class Font(object): def is_custom_font(self): return get_custom_font_dir() in self.path - def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim=False): + def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim_option=0): + """Render text into an SVG group element.""" self._load_variants() @@ -206,7 +208,7 @@ class Font(object): position.y += self.leading if self.auto_satin and len(destination_group) > 0: - self._apply_auto_satin(destination_group, trim) + self._apply_auto_satin(destination_group) # make sure font stroke styles have always a similar look for element in destination_group.iterdescendants(SVG_PATH_TAG): @@ -220,6 +222,8 @@ class Font(object): style += inkex.Style("stroke-width:0.5px") element.set('style', '%s' % style.to_str()) + # add trims + self._add_trims(destination_group, text, trim_option, back_and_forth) # make sure necessary marker and command symbols are in the defs section self._ensure_command_symbols(destination_group) self._ensure_marker_symbols(destination_group) @@ -309,6 +313,10 @@ class Font(object): self._update_commands(node, glyph) + # this is used to recognize a glyph layer later in the process + # because this is not unique it will be overwritten by inkscape when inserted into the document + node.set("id", "glyph") + return node def _update_commands(self, node, glyph): @@ -329,6 +337,58 @@ class Font(object): c.set(CONNECTION_END, "#%s" % new_element_id) c.set(CONNECTION_START, "#%s" % new_symbol_id) + def _add_trims(self, destination_group, text, trim_option, back_and_forth): + """ + trim_option == 0 --> no trims + trim_option == 1 --> trim at the end of each line + trim_option == 2 --> trim after each word + trim_option == 3 --> trim after each letter + """ + if trim_option == 0: + return + + # reverse every second line of text if back and forth is true and strip spaces + text = text.splitlines() + text = [t[::-1].strip() if i % 2 != 0 and back_and_forth else t.strip() for i, t in enumerate(text)] + text = "\n".join(text) + + i = -1 + space_indices = [i for i, t in enumerate(text) if t == " "] + line_break_indices = [i for i, t in enumerate(text) if t == "\n"] + for group in destination_group.iterdescendants(SVG_GROUP_TAG): + # make sure we are only looking at glyph groups + if group.get("id") != "glyph": + continue + + i += 1 + while i in space_indices + line_break_indices: + i += 1 + + # letter + if trim_option == 3: + self._process_trim(group) + # word + elif trim_option == 2 and i+1 in space_indices + line_break_indices: + self._process_trim(group) + # line + elif trim_option == 1 and i+1 in line_break_indices: + self._process_trim(group) + + def _process_trim(self, group): + # find the last path that does not carry a marker and add a trim there + for path_child in group.iterdescendants(EMBROIDERABLE_TAGS): + if not has_marker(path_child): + path = path_child + if path.get('style') and "fill" in path.get('style'): + element = FillStitch(path) + else: + element = Stroke(path) + + if element.shape: + element_id = "%s_%s" % (element.node.get('id'), randint(0, 9999)) + element.node.set("id", element_id) + add_commands(element, ['trim']) + def _ensure_command_symbols(self, group): # collect commands commands = set() @@ -349,16 +409,14 @@ class Font(object): for element in marked_elements: element.style['marker-start'] = "url(#inkstitch-%s-marker)" % marker - def _apply_auto_satin(self, group, trim): + def _apply_auto_satin(self, group): """Apply Auto-Satin to an SVG XML node tree with an svg:g at its root. The group's contents will be replaced with the results of the auto- satin operation. Any nested svg:g elements will be removed. """ - # TODO: trim option for non-auto-route - elements = nodes_to_elements(group.iterdescendants(SVG_PATH_TAG)) if elements: - auto_satin(elements, preserve_order=True, trim=trim) + auto_satin(elements, preserve_order=True, trim=False) diff --git a/lib/lettering/font_variant.py b/lib/lettering/font_variant.py index a7f353fe..ecdba137 100644 --- a/lib/lettering/font_variant.py +++ b/lib/lettering/font_variant.py @@ -69,7 +69,10 @@ class FontVariant(object): self._clean_group(layer) layer.attrib[INKSCAPE_LABEL] = layer.attrib[INKSCAPE_LABEL].replace("GlyphLayer-", "", 1) glyph_name = layer.attrib[INKSCAPE_LABEL] - self.glyphs[glyph_name] = Glyph(layer) + try: + self.glyphs[glyph_name] = Glyph(layer) + except AttributeError: + pass def _clean_group(self, group): # We'll repurpose the layer as a container group labelled with the diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index ed4ff655..60a0cb7d 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -38,7 +38,7 @@ def row_num(point, angle, row_spacing): def adjust_stagger(stitch, angle, row_spacing, max_stitch_length, staggers): if staggers == 0: - staggers = 1 # sanity check to avoid division by zero. + staggers = 1 # sanity check to avoid division by zero. this_row_num = row_num(stitch, angle, row_spacing) stagger_phase = (this_row_num / staggers) % 1 stagger_offset = stagger_phase * max_stitch_length diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 6a92f8e8..14c58b7a 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -149,7 +149,7 @@ def take_only_line_strings(thing): def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num): if num_staggers == 0: - num_staggers = 1 # sanity check to avoid division by zero. + num_staggers = 1 # sanity check to avoid division by zero. start = ((row_num / num_staggers) % 1) * max_stitch_length projections = np.arange(start, line.length, max_stitch_length) points = np.array([line.interpolate(projection).coords[0] for projection in projections]) diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py index 6a0ef7f0..67362f12 100644 --- a/lib/stitches/ripple_stitch.py +++ b/lib/stitches/ripple_stitch.py @@ -74,7 +74,7 @@ def _get_satin_ripple_helper_lines(stroke): length = stroke.grid_size or stroke.running_stitch_length # use satin column points for satin like build ripple stitches - rail_points = SatinColumn(stroke.node).plot_points_on_rails(length, 0) + rail_points = SatinColumn(stroke.node).plot_points_on_rails(length) steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent) diff --git a/lib/svg/tags.py b/lib/svg/tags.py index a4dfa0ba..4743438b 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -53,6 +53,7 @@ inkstitch_attribs = [ 'force_lock_stitches', # clone 'clone', + 'flip_angle', # polyline 'polyline', # fill @@ -103,21 +104,24 @@ inkstitch_attribs = [ 'short_stitch_distance_mm', 'short_stitch_inset', 'running_stitch_length_mm', + 'swap_satin_rails', 'center_walk_underlay', 'center_walk_underlay_stitch_length_mm', 'center_walk_underlay_repeats', + 'center_walk_underlay_position', 'contour_underlay', 'contour_underlay_stitch_length_mm', 'contour_underlay_inset_mm', + 'contour_underlay_inset_percent', 'zigzag_underlay', 'zigzag_spacing_mm', 'zigzag_underlay_inset_mm', + 'zigzag_underlay_inset_percent', 'zigzag_underlay_spacing_mm', 'zigzag_underlay_max_stitch_length_mm', 'e_stitch', 'pull_compensation_mm', 'pull_compensation_percent', - 'pull_compensation_rails', 'stroke_first', 'random_split_factor', 'random_first_rail_factor_in', |
