Merge pull request #2284 from inkstitch/kaalleen/various-fixes

pull/2194/head
Kaalleen 2023-05-16 16:58:17 +02:00 zatwierdzone przez GitHub
commit 1193b4f206
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
11 zmienionych plików z 181 dodań i 120 usunięć

Wyświetl plik

@ -352,14 +352,14 @@ class FillStitch(EmbroideryElement):
'Also used for meander and circular fill.'), 'Also used for meander and circular fill.'),
unit='mm', unit='mm',
type='float', type='float',
default=1.5, default=2.5,
select_items=[('fill_method', 'auto_fill'), select_items=[('fill_method', 'auto_fill'),
('fill_method', 'guided_fill'), ('fill_method', 'guided_fill'),
('fill_method', 'meander_fill'), ('fill_method', 'meander_fill'),
('fill_method', 'circular_fill')], ('fill_method', 'circular_fill')],
sort_index=31) sort_index=31)
def running_stitch_length(self): 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 @property
@param('running_stitch_tolerance_mm', @param('running_stitch_tolerance_mm',
@ -607,13 +607,13 @@ class FillStitch(EmbroideryElement):
def validation_errors(self): def validation_errors(self):
if not self.shape_is_valid(self.shape): if not self.shape_is_valid(self.shape):
why = explain_validity(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)) yield InvalidShapeError((x, y))
def validation_warnings(self): # noqa: C901 def validation_warnings(self): # noqa: C901
if not self.shape_is_valid(self.original_shape): if not self.shape_is_valid(self.original_shape):
why = explain_validity(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: if "Hole lies outside shell" in message:
yield UnconnectedWarning((x, y)) yield UnconnectedWarning((x, y))
else: else:

Wyświetl plik

@ -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

Wyświetl plik

@ -101,10 +101,10 @@ class Stroke(EmbroideryElement):
unit='mm', unit='mm',
type='float', type='float',
select_items=[('stroke_method', 'running_stitch'), ('stroke_method', 'ripple_stitch')], select_items=[('stroke_method', 'running_stitch'), ('stroke_method', 'ripple_stitch')],
default=1.5, default=2.5,
sort_index=4) sort_index=4)
def running_stitch_length(self): 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 @property
@param('running_stitch_tolerance_mm', @param('running_stitch_tolerance_mm',
@ -261,6 +261,27 @@ class Stroke(EmbroideryElement):
def reverse(self): def reverse(self):
return self.get_boolean_param("reverse", False) 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 @property
@param('grid_size_mm', @param('grid_size_mm',
_('Grid size'), _('Grid size'),
@ -269,7 +290,7 @@ class Stroke(EmbroideryElement):
default=0, default=0,
unit='mm', unit='mm',
select_items=[('stroke_method', 'ripple_stitch')], select_items=[('stroke_method', 'ripple_stitch')],
sort_index=15) sort_index=16)
@cache @cache
def grid_size(self): def grid_size(self):
return abs(self.get_float_param("grid_size_mm", 0)) 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 # 0: xy, 1: x, 2: y, 3: none
options=["X Y", "X", "Y", _("None")], options=["X Y", "X", "Y", _("None")],
select_items=[('stroke_method', 'ripple_stitch')], select_items=[('stroke_method', 'ripple_stitch')],
sort_index=16) sort_index=17)
def scale_axis(self): def scale_axis(self):
return self.get_int_param('scale_axis', 0) return self.get_int_param('scale_axis', 0)
@ -295,7 +316,7 @@ class Stroke(EmbroideryElement):
unit='%', unit='%',
default=100, default=100,
select_items=[('stroke_method', 'ripple_stitch')], select_items=[('stroke_method', 'ripple_stitch')],
sort_index=17) sort_index=18)
def scale_start(self): def scale_start(self):
return self.get_float_param('scale_start', 100.0) return self.get_float_param('scale_start', 100.0)
@ -307,7 +328,7 @@ class Stroke(EmbroideryElement):
unit='%', unit='%',
default=0.0, default=0.0,
select_items=[('stroke_method', 'ripple_stitch')], select_items=[('stroke_method', 'ripple_stitch')],
sort_index=18) sort_index=19)
def scale_end(self): def scale_end(self):
return self.get_float_param('scale_end', 0.0) return self.get_float_param('scale_end', 0.0)
@ -318,7 +339,7 @@ class Stroke(EmbroideryElement):
type='boolean', type='boolean',
default=True, default=True,
select_items=[('stroke_method', 'ripple_stitch')], select_items=[('stroke_method', 'ripple_stitch')],
sort_index=19) sort_index=20)
@cache @cache
def rotate_ripples(self): def rotate_ripples(self):
return self.get_boolean_param("rotate_ripples", True) return self.get_boolean_param("rotate_ripples", True)
@ -331,7 +352,7 @@ class Stroke(EmbroideryElement):
default=0, default=0,
options=(_("flat"), _("point")), options=(_("flat"), _("point")),
select_items=[('stroke_method', 'ripple_stitch')], select_items=[('stroke_method', 'ripple_stitch')],
sort_index=20) sort_index=21)
@cache @cache
def join_style(self): def join_style(self):
return self.get_int_param('join_style', 0) return self.get_int_param('join_style', 0)

Wyświetl plik

@ -99,6 +99,7 @@ class ConvertToSatin(InkstitchExtension):
path[0] = start.as_tuple() path[0] = start.as_tuple()
def remove_duplicate_points(self, path): 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)] return [point for point, repeats in groupby(path)]
def join_style_args(self, element): def join_style_args(self, element):

Wyświetl plik

@ -3,17 +3,18 @@
# Copyright (c) 2010 Authors # Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. # 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.geometry import Point
from shapely.ops import nearest_points from shapely.ops import nearest_points, split
from ..commands import add_commands from ..commands import add_commands
from ..elements import FillStitch from ..elements import FillStitch
from ..elements.gradient_fill import gradient_shapes_and_attributes
from ..i18n import _ 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 ..svg.tags import INKSTITCH_ATTRIBS
from .commands import CommandsExtension from .commands import CommandsExtension
from .duplicate_params import get_inkstitch_attributes from .duplicate_params import get_inkstitch_attributes
@ -28,6 +29,9 @@ class GradientBlocks(CommandsExtension):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
CommandsExtension.__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") self.arg_parser.add_argument("-e", "--end-row-spacing", type=float, default=0.0, dest="end_row_spacing")
def effect(self): def effect(self):
@ -53,35 +57,45 @@ class GradientBlocks(CommandsExtension):
fill_shapes.reverse() fill_shapes.reverse()
attributes.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_color = None
previous_element = None previous_element = None
for i, shape in enumerate(fill_shapes): for i, shape in enumerate(fill_shapes):
color = attributes[i]['color'] color = attributes[i]['color']
style['fill'] = 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 = degrees(attributes[i]['angle'])
angle = f'{angle: .2f}'
d = "M " + " ".join([f'{x}, {y}' for x, y in list(shape.exterior.coords)]) + " Z" d = "M " + " ".join([f'{x}, {y}' for x, y in list(shape.exterior.coords)]) + " Z"
block = PathElement(attrib={ block = PathElement(attrib={
"id": self.uniqueId("path"), "id": self.uniqueId("path"),
"style": str(style), "style": str(style),
"transform": correction_transform, "transform": correction_transform,
"d": d, "d": d,
INKSTITCH_ATTRIBS['angle']: f'{angle: .2f}' INKSTITCH_ATTRIBS['angle']: angle
}) })
# apply parameters from original element # apply parameters from original element
params = get_inkstitch_attributes(element.node) params = get_inkstitch_attributes(element.node)
for attrib in params: for attrib in params:
block.attrib[attrib] = str(element.node.attrib[attrib]) 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 # disable underlay and underpath
block.set('inkstitch:fill_underlay', False) block.set('inkstitch:fill_underlay', False)
block.set('inkstitch:underpath', 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) parent.insert(index, block)
if previous_color == color: if previous_color == color:
@ -106,6 +120,77 @@ class GradientBlocks(CommandsExtension):
return Point(pos) 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__': if __name__ == '__main__':
e = GradientBlocks() e = GradientBlocks()
e.effect() e.effect()

Wyświetl plik

@ -5,7 +5,7 @@
import inkex import inkex
from ..elements import SatinColumn, Stroke from ..elements import EmptyDObject, SatinColumn, Stroke
from ..i18n import _ from ..i18n import _
from ..svg.tags import ORIGINAL_D, PATH_EFFECT, SODIPODI_NODETYPES from ..svg.tags import ORIGINAL_D, PATH_EFFECT, SODIPODI_NODETYPES
from .base import InkstitchExtension 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): 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. # 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 return
pattern = self.options.pattern pattern = self.options.pattern

Wyświetl plik

@ -14,8 +14,8 @@ class UpdateSvg(InkstitchExtension):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
InkstitchExtension.__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 # TODO: When there are more legacy versions than only one, this can be transformed into a user input
# inkstitch_svg_version history: 1 -> v2.3.0 # inkstitch_svg_version history: 1 -> v3.0.0, May 2023
self.update_from = 0 self.update_from = 0
def effect(self): def effect(self):

Wyświetl plik

@ -182,7 +182,10 @@ class Font(object):
return self.name + '*' return self.name + '*'
def is_custom_font(self): 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): def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim_option=0, use_trim_symbols=False):

Wyświetl plik

@ -103,9 +103,7 @@ def _get_satin_line_count(stroke, pairs):
if shortest_line_len == 0 or length < shortest_line_len: if shortest_line_len == 0 or length < shortest_line_len:
shortest_line_len = length shortest_line_len = length
num_lines = ceil(shortest_line_len / stroke.min_line_dist) num_lines = ceil(shortest_line_len / stroke.min_line_dist)
if stroke.join_style == 1: return _line_count_adjust(stroke, num_lines)
num_lines += 1
return num_lines
def _get_target_line_count(stroke, target, outline): 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 num_lines = stroke.line_count
else: else:
num_lines = ceil(guide_line.length / stroke.min_line_dist) 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: if stroke.is_closed or stroke.join_style == 1:
# for flat join styles we need to add an other line
num_lines += 1 num_lines += 1
return num_lines return num_lines

Wyświetl plik

@ -104,7 +104,7 @@ def _update_to_one(element): # noqa: C901
element.remove_param('e_stitch') element.remove_param('e_stitch')
element.set_param('satin_method', '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 # reverse_rails defaults to Automatic, but we should never reverse an
# old satin automatically, only new ones # old satin automatically, only new ones
element.set_param('reverse_rails', 'none') element.set_param('reverse_rails', 'none')
@ -112,6 +112,9 @@ def _update_to_one(element): # noqa: C901
# default setting for fill_underlay has changed # default setting for fill_underlay has changed
if legacy_attribs and not element.get_param('fill_underlay', ""): if legacy_attribs and not element.get_param('fill_underlay', ""):
element.set_param('fill_underlay', False) 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 # convert legacy stroke_method
if element.get_style("stroke") and not element.node.get('inkscape:connection-start', None): if element.get_style("stroke") and not element.node.get('inkscape:connection-start', None):

Wyświetl plik

@ -11,12 +11,26 @@
</submenu> </submenu>
</effects-menu> </effects-menu>
</effect> </effect>
<param name="end-row-spacing" <param name="notebook" type="notebook">
gui-text="End row spacing" <page name="options" gui-text="Options">
gui-description="Set to zero to use twice the row spacing value" <param name="end-row-spacing"
type="float" gui-text="End row spacing"
min="0" max="100" gui-description="Set to zero to use twice the row spacing value"
indents="1">0.5</param> 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> <script>
{{ command_tag | safe }} {{ command_tag | safe }}
</script> </script>