summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2023-05-16 16:58:17 +0200
committerGitHub <noreply@github.com>2023-05-16 16:58:17 +0200
commit1193b4f2062d2bc4114fefe29992553c59200090 (patch)
tree5f43833eed5facdf9ec35d0b755a6fe492b9bfa2
parent02738ec46e333a53c9970fd936340e88f6108ffb (diff)
parent66068208e87f5464fa6b8038b74fe95de6b21bb9 (diff)
Merge pull request #2284 from inkstitch/kaalleen/various-fixes
-rw-r--r--lib/elements/fill_stitch.py8
-rw-r--r--lib/elements/gradient_fill.py79
-rw-r--r--lib/elements/stroke.py37
-rw-r--r--lib/extensions/convert_to_satin.py1
-rw-r--r--lib/extensions/gradient_blocks.py113
-rw-r--r--lib/extensions/stroke_to_lpe_satin.py7
-rw-r--r--lib/extensions/update_svg.py4
-rw-r--r--lib/lettering/font.py5
-rw-r--r--lib/stitches/ripple_stitch.py16
-rw-r--r--lib/update.py5
-rw-r--r--templates/gradient_blocks.xml26
11 files changed, 181 insertions, 120 deletions
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py
index 912f3ba6..91cad563 100644
--- a/lib/elements/fill_stitch.py
+++ b/lib/elements/fill_stitch.py
@@ -352,14 +352,14 @@ class FillStitch(EmbroideryElement):
'Also used for meander and circular fill.'),
unit='mm',
type='float',
- default=1.5,
+ default=2.5,
select_items=[('fill_method', 'auto_fill'),
('fill_method', 'guided_fill'),
('fill_method', 'meander_fill'),
('fill_method', 'circular_fill')],
sort_index=31)
def running_stitch_length(self):
- return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
+ return max(self.get_float_param("running_stitch_length_mm", 2.5), 0.01)
@property
@param('running_stitch_tolerance_mm',
@@ -607,13 +607,13 @@ class FillStitch(EmbroideryElement):
def validation_errors(self):
if not self.shape_is_valid(self.shape):
why = explain_validity(self.shape)
- message, x, y = re.findall(r".+?(?=\[)|-?\d+(?:\.\d+)?", why)
+ message, x, y = re.match(r"(?P<message>.+)\[(?P<x>.+)\s(?P<y>.+)\]", why).groups()
yield InvalidShapeError((x, y))
def validation_warnings(self): # noqa: C901
if not self.shape_is_valid(self.original_shape):
why = explain_validity(self.original_shape)
- message, x, y = re.findall(r".+?(?=\[)|-?\d+(?:\.\d+)?", why)
+ message, x, y = re.match(r"(?P<message>.+)\[(?P<x>.+)\s(?P<y>.+)\]", why).groups()
if "Hole lies outside shell" in message:
yield UnconnectedWarning((x, y))
else:
diff --git a/lib/elements/gradient_fill.py b/lib/elements/gradient_fill.py
deleted file mode 100644
index 18443368..00000000
--- a/lib/elements/gradient_fill.py
+++ /dev/null
@@ -1,79 +0,0 @@
-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 PIXELS_PER_MM, 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 / PIXELS_PER_MM * 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/stroke.py b/lib/elements/stroke.py
index 16689901..ac54908b 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -101,10 +101,10 @@ class Stroke(EmbroideryElement):
unit='mm',
type='float',
select_items=[('stroke_method', 'running_stitch'), ('stroke_method', 'ripple_stitch')],
- default=1.5,
+ default=2.5,
sort_index=4)
def running_stitch_length(self):
- return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
+ return max(self.get_float_param("running_stitch_length_mm", 2.5), 0.01)
@property
@param('running_stitch_tolerance_mm',
@@ -261,6 +261,27 @@ class Stroke(EmbroideryElement):
def reverse(self):
return self.get_boolean_param("reverse", False)
+ _reverse_rails_options = [ParamOption('automatic', _('Automatic')),
+ ParamOption('none', _("Don't reverse")),
+ ParamOption('first', _('Reverse first rail')),
+ ParamOption('second', _('Reverse second rail')),
+ ParamOption('both', _('Reverse both rails'))
+ ]
+
+ @property
+ @param(
+ 'reverse_rails',
+ _('Reverse rails'),
+ tooltip=_('Reverse satin ripple rails. ' +
+ 'Default: automatically detect and fix a reversed rail.'),
+ type='combo',
+ options=_reverse_rails_options,
+ default='automatic',
+ select_items=[('stroke_method', 'ripple_stitch')],
+ sort_index=15)
+ def reverse_rails(self):
+ return self.get_param('reverse_rails', 'automatic')
+
@property
@param('grid_size_mm',
_('Grid size'),
@@ -269,7 +290,7 @@ class Stroke(EmbroideryElement):
default=0,
unit='mm',
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=15)
+ sort_index=16)
@cache
def grid_size(self):
return abs(self.get_float_param("grid_size_mm", 0))
@@ -283,7 +304,7 @@ class Stroke(EmbroideryElement):
# 0: xy, 1: x, 2: y, 3: none
options=["X Y", "X", "Y", _("None")],
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=16)
+ sort_index=17)
def scale_axis(self):
return self.get_int_param('scale_axis', 0)
@@ -295,7 +316,7 @@ class Stroke(EmbroideryElement):
unit='%',
default=100,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=17)
+ sort_index=18)
def scale_start(self):
return self.get_float_param('scale_start', 100.0)
@@ -307,7 +328,7 @@ class Stroke(EmbroideryElement):
unit='%',
default=0.0,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=18)
+ sort_index=19)
def scale_end(self):
return self.get_float_param('scale_end', 0.0)
@@ -318,7 +339,7 @@ class Stroke(EmbroideryElement):
type='boolean',
default=True,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=19)
+ sort_index=20)
@cache
def rotate_ripples(self):
return self.get_boolean_param("rotate_ripples", True)
@@ -331,7 +352,7 @@ class Stroke(EmbroideryElement):
default=0,
options=(_("flat"), _("point")),
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=20)
+ sort_index=21)
@cache
def join_style(self):
return self.get_int_param('join_style', 0)
diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py
index 5512a095..7a36ce21 100644
--- a/lib/extensions/convert_to_satin.py
+++ b/lib/extensions/convert_to_satin.py
@@ -99,6 +99,7 @@ class ConvertToSatin(InkstitchExtension):
path[0] = start.as_tuple()
def remove_duplicate_points(self, path):
+ path = [[round(coord, 4) for coord in point] for point in path]
return [point for point, repeats in groupby(path)]
def join_style_args(self, element):
diff --git a/lib/extensions/gradient_blocks.py b/lib/extensions/gradient_blocks.py
index 563e3127..5d8318b6 100644
--- a/lib/extensions/gradient_blocks.py
+++ b/lib/extensions/gradient_blocks.py
@@ -3,17 +3,18 @@
# 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 math import degrees, pi
-from inkex import DirectedLineSegment, PathElement, errormsg
+from inkex import DirectedLineSegment, PathElement, Transform, errormsg
+from shapely import geometry as shgeo
+from shapely.affinity import affine_transform, rotate
from shapely.geometry import Point
-from shapely.ops import nearest_points
+from shapely.ops import nearest_points, split
from ..commands import add_commands
from ..elements import FillStitch
-from ..elements.gradient_fill import gradient_shapes_and_attributes
from ..i18n import _
-from ..svg import get_correction_transform
+from ..svg import PIXELS_PER_MM, get_correction_transform
from ..svg.tags import INKSTITCH_ATTRIBS
from .commands import CommandsExtension
from .duplicate_params import get_inkstitch_attributes
@@ -28,6 +29,9 @@ class GradientBlocks(CommandsExtension):
def __init__(self, *args, **kwargs):
CommandsExtension.__init__(self, *args, **kwargs)
+ self.arg_parser.add_argument("--notebook", type=str, default=0.0)
+ self.arg_parser.add_argument("--options", type=str, default=0.0)
+ self.arg_parser.add_argument("--info", type=str, default=0.0)
self.arg_parser.add_argument("-e", "--end-row-spacing", type=float, default=0.0, dest="end_row_spacing")
def effect(self):
@@ -53,35 +57,45 @@ class GradientBlocks(CommandsExtension):
fill_shapes.reverse()
attributes.reverse()
+ if self.options.end_row_spacing != 0:
+ end_row_spacing = self.options.end_row_spacing
+ else:
+ end_row_spacing = element.row_spacing / PIXELS_PER_MM * 2
+ end_row_spacing = f'{end_row_spacing: .2f}'
+
previous_color = None
previous_element = None
for i, shape in enumerate(fill_shapes):
color = attributes[i]['color']
style['fill'] = color
- end_row_spacing = attributes[i]['end_row_spacing'] or None
+ is_gradient = attributes[i]['is_gradient']
angle = degrees(attributes[i]['angle'])
+ angle = f'{angle: .2f}'
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}'
+ INKSTITCH_ATTRIBS['angle']: angle
})
# apply parameters from original element
params = get_inkstitch_attributes(element.node)
for attrib in params:
block.attrib[attrib] = str(element.node.attrib[attrib])
- # set end_row_spacing
- if end_row_spacing:
- if self.options.end_row_spacing != 0:
- end_row_spacing = self.options.end_row_spacing
- block.set('inkstitch:end_row_spacing_mm', f'{end_row_spacing: .2f}')
- else:
- block.pop('inkstitch:end_row_spacing_mm')
# disable underlay and underpath
block.set('inkstitch:fill_underlay', False)
block.set('inkstitch:underpath', False)
+ # set end_row_spacing
+ if is_gradient:
+ block.set('inkstitch:end_row_spacing_mm', end_row_spacing)
+ else:
+ block.pop('inkstitch:end_row_spacing_mm')
+ # use underlay to compensate for higher density in the gradient parts
+ block.set('inkstitch:fill_underlay', True)
+ block.set('inkstitch:fill_underlay_angle', angle)
+ block.set('inkstitch:fill_underlay_row_spacing_mm', end_row_spacing)
+
parent.insert(index, block)
if previous_color == color:
@@ -106,6 +120,77 @@ class GradientBlocks(CommandsExtension):
return Point(pos)
+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
+ is_gradient = False
+ 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({'color': previous_color, 'angle': stitch_angle, 'is_gradient': is_gradient})
+ polygons.append(poly)
+ attributes.append({'color': color, 'angle': stitch_angle + pi, 'is_gradient': is_gradient})
+ else:
+ shape_rest.append(poly)
+ shape = shgeo.MultiPolygon(shape_rest)
+ previous_color = color
+ is_gradient = True
+ # 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, 'is_gradient': is_gradient})
+ stitch_angle += pi
+ else:
+ is_gradient = False
+ for s in shape.geoms:
+ polygons.append(s)
+ attributes.append({'color': stop_styles[-1]['stop-color'], 'angle': stitch_angle, 'is_gradient': is_gradient})
+ return polygons, attributes
+
+
if __name__ == '__main__':
e = GradientBlocks()
e.effect()
diff --git a/lib/extensions/stroke_to_lpe_satin.py b/lib/extensions/stroke_to_lpe_satin.py
index 5aa873e9..b89e471c 100644
--- a/lib/extensions/stroke_to_lpe_satin.py
+++ b/lib/extensions/stroke_to_lpe_satin.py
@@ -5,7 +5,7 @@
import inkex
-from ..elements import SatinColumn, Stroke
+from ..elements import EmptyDObject, SatinColumn, Stroke
from ..i18n import _
from ..svg.tags import ORIGINAL_D, PATH_EFFECT, SODIPODI_NODETYPES
from .base import InkstitchExtension
@@ -35,7 +35,10 @@ class StrokeToLpeSatin(InkstitchExtension):
if not any((isinstance(item, Stroke) or isinstance(item, SatinColumn)) for item in self.elements):
# L10N: Convert To Satin extension, user selected one or more objects that were not lines.
- inkex.errormsg(_("Please select at least one stroke to convert to a satin column."))
+ if any(isinstance(item, EmptyDObject) for item in self.elements):
+ inkex.errormsg(_("This element has lost its path information. Please move the element slightly back and forth before you try again."))
+ else:
+ inkex.errormsg(_("Please select at least one stroke to convert to a satin column."))
return
pattern = self.options.pattern
diff --git a/lib/extensions/update_svg.py b/lib/extensions/update_svg.py
index 51960cb2..0f0609be 100644
--- a/lib/extensions/update_svg.py
+++ b/lib/extensions/update_svg.py
@@ -14,8 +14,8 @@ class UpdateSvg(InkstitchExtension):
def __init__(self, *args, **kwargs):
InkstitchExtension.__init__(self, *args, **kwargs)
- # TODO: When there are more legacy versions than only one, this can be transformed in a user input
- # inkstitch_svg_version history: 1 -> v2.3.0
+ # TODO: When there are more legacy versions than only one, this can be transformed into a user input
+ # inkstitch_svg_version history: 1 -> v3.0.0, May 2023
self.update_from = 0
def effect(self):
diff --git a/lib/lettering/font.py b/lib/lettering/font.py
index f5517797..77f17e7f 100644
--- a/lib/lettering/font.py
+++ b/lib/lettering/font.py
@@ -182,7 +182,10 @@ class Font(object):
return self.name + '*'
def is_custom_font(self):
- return get_custom_font_dir() in self.path
+ custom_dir = get_custom_font_dir()
+ if not custom_dir:
+ return False
+ return custom_dir in self.path
def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim_option=0, use_trim_symbols=False):
diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py
index 4e1c563e..40a522eb 100644
--- a/lib/stitches/ripple_stitch.py
+++ b/lib/stitches/ripple_stitch.py
@@ -103,9 +103,7 @@ def _get_satin_line_count(stroke, pairs):
if shortest_line_len == 0 or length < shortest_line_len:
shortest_line_len = length
num_lines = ceil(shortest_line_len / stroke.min_line_dist)
- if stroke.join_style == 1:
- num_lines += 1
- return num_lines
+ return _line_count_adjust(stroke, num_lines)
def _get_target_line_count(stroke, target, outline):
@@ -117,7 +115,19 @@ def _get_guided_line_count(stroke, guide_line):
num_lines = stroke.line_count
else:
num_lines = ceil(guide_line.length / stroke.min_line_dist)
+ return _line_count_adjust(stroke, num_lines)
+
+
+def _line_count_adjust(stroke, num_lines):
+ if stroke.min_line_dist and stroke.line_count % 2 != num_lines % 2:
+ # We want the line count always to be either even or odd - depending on the line count value.
+ # So that the end point stays the same even if the design is resized. This is necessary to enable
+ # the user to carefully plan the output and and connect the end point to the following object
+ num_lines -= 1
+ # ensure minimum line count
+ num_lines = max(1, num_lines)
if stroke.is_closed or stroke.join_style == 1:
+ # for flat join styles we need to add an other line
num_lines += 1
return num_lines
diff --git a/lib/update.py b/lib/update.py
index 6287a77c..b0bfcdfa 100644
--- a/lib/update.py
+++ b/lib/update.py
@@ -104,7 +104,7 @@ def _update_to_one(element): # noqa: C901
element.remove_param('e_stitch')
element.set_param('satin_method', 'e_stitch')
- if element.get_boolean_param('satin_column', False):
+ if element.get_boolean_param('satin_column', False) or element.get_int_param('stroke_method', 0) == 1:
# reverse_rails defaults to Automatic, but we should never reverse an
# old satin automatically, only new ones
element.set_param('reverse_rails', 'none')
@@ -112,6 +112,9 @@ def _update_to_one(element): # noqa: C901
# default setting for fill_underlay has changed
if legacy_attribs and not element.get_param('fill_underlay', ""):
element.set_param('fill_underlay', False)
+ # default setting for running stitch length has changed (fills and strokes, not satins)
+ if not element.get_boolean_param('satin_column', False) and element.get_float_param('running_stitch_length_mm', None) is None:
+ element.set_param('running_stitch_length_mm', 1.5)
# convert legacy stroke_method
if element.get_style("stroke") and not element.node.get('inkscape:connection-start', None):
diff --git a/templates/gradient_blocks.xml b/templates/gradient_blocks.xml
index 7129f2bd..f824d514 100644
--- a/templates/gradient_blocks.xml
+++ b/templates/gradient_blocks.xml
@@ -11,12 +11,26 @@
</submenu>
</effects-menu>
</effect>
- <param name="end-row-spacing"
- gui-text="End row spacing"
- gui-description="Set to zero to use twice the row spacing value"
- type="float"
- min="0" max="100"
- indents="1">0.5</param>
+ <param name="notebook" type="notebook">
+ <page name="options" gui-text="Options">
+ <param name="end-row-spacing"
+ gui-text="End row spacing"
+ gui-description="Set to zero to use twice the row spacing value"
+ type="float"
+ min="0" max="100"
+ precision="2"
+ indents="1">0</param>
+ </page>
+ <page name="info" gui-text="Help">
+ <label appearance="header">Converts a fill with a linear color gradient into color blocks with variable row spacing.</label>
+ <spacer />
+ <label>This may add density at the center.</label>
+ <spacer />
+ <label>If necessary adapt the end row spacing value after the conversion with the params dialog.</label>
+ <spacer />
+ <label appearance="url">https://inkstitch.org/docs/fill-tools/#convert-to-gradient-blocks</label>
+ </page>
+ </param>
<script>
{{ command_tag | safe }}
</script>