summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/commands.py8
-rw-r--r--lib/elements/clone.py81
-rw-r--r--lib/elements/element.py27
-rw-r--r--lib/elements/fill_stitch.py3
-rw-r--r--lib/elements/gradient_fill.py79
-rw-r--r--lib/elements/satin_column.py195
-rw-r--r--lib/elements/stroke.py23
-rw-r--r--lib/extensions/__init__.py2
-rw-r--r--lib/extensions/gradient_blocks.py69
-rw-r--r--lib/extensions/lettering.py31
-rw-r--r--lib/lettering/font.py80
-rw-r--r--lib/lettering/font_variant.py5
-rw-r--r--lib/stitches/fill.py2
-rw-r--r--lib/stitches/guided_fill.py2
-rw-r--r--lib/stitches/ripple_stitch.py2
-rw-r--r--lib/svg/tags.py6
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',