kopia lustrzana https://github.com/inkstitch/inkstitch
Add linear gradient fill (#2587)
rodzic
0b12922d3f
commit
3bd92265b2
|
@ -526,6 +526,14 @@ class EmbroideryElement(object):
|
|||
def _get_guides_cache_key_data(self):
|
||||
return get_marker_elements_cache_key_data(self.node, "guide-line")
|
||||
|
||||
def _get_gradient_cache_key_data(self):
|
||||
gradient = {}
|
||||
if hasattr(self, 'gradient') and self.gradient is not None:
|
||||
gradient['stops'] = self.gradient.stop_offsets
|
||||
gradient['orientation'] = [self.gradient.x1(), self.gradient.x2(), self.gradient.y1(), self.gradient.y2()]
|
||||
gradient['styles'] = [(style['stop-color'], style['stop-opacity']) for style in self.gradient.stop_styles]
|
||||
return gradient
|
||||
|
||||
def get_cache_key_data(self, previous_stitch):
|
||||
return []
|
||||
|
||||
|
@ -535,6 +543,7 @@ class EmbroideryElement(object):
|
|||
cache_key_generator.update(self.get_params_and_values())
|
||||
cache_key_generator.update(self.parse_path())
|
||||
cache_key_generator.update(list(self._get_specified_style().items()))
|
||||
cache_key_generator.update(self._get_gradient_cache_key_data())
|
||||
cache_key_generator.update(previous_stitch)
|
||||
cache_key_generator.update([(c.command, c.target_point) for c in self.commands])
|
||||
cache_key_generator.update(self._get_patterns_cache_key_data())
|
||||
|
|
|
@ -11,6 +11,7 @@ import numpy as np
|
|||
from inkex import Transform
|
||||
from shapely import geometry as shgeo
|
||||
from shapely.errors import GEOSException
|
||||
from shapely.ops import nearest_points
|
||||
from shapely.validation import explain_validity, make_valid
|
||||
|
||||
from .. import tiles
|
||||
|
@ -18,8 +19,8 @@ from ..i18n import _
|
|||
from ..marker import get_marker_elements
|
||||
from ..stitch_plan import StitchGroup
|
||||
from ..stitches import (auto_fill, circular_fill, contour_fill, guided_fill,
|
||||
legacy_fill)
|
||||
from ..stitches.meander_fill import meander_fill
|
||||
legacy_fill, linear_gradient_fill, meander_fill)
|
||||
from ..stitches.linear_gradient_fill import gradient_angle
|
||||
from ..svg import PIXELS_PER_MM, get_node_transform
|
||||
from ..svg.clip import get_clip_path
|
||||
from ..svg.tags import INKSCAPE_LABEL
|
||||
|
@ -114,6 +115,7 @@ class FillStitch(EmbroideryElement):
|
|||
ParamOption('guided_fill', _("Guided Fill")),
|
||||
ParamOption('meander_fill', _("Meander Fill")),
|
||||
ParamOption('circular_fill', _("Circular Fill")),
|
||||
ParamOption('linear_gradient_fill', _("Linear Gradient Fill")),
|
||||
ParamOption('legacy_fill', _("Legacy Fill"))]
|
||||
|
||||
@property
|
||||
|
@ -226,7 +228,8 @@ class FillStitch(EmbroideryElement):
|
|||
select_items=[('fill_method', 'auto_fill'),
|
||||
('fill_method', 'guided_fill'),
|
||||
('fill_method', 'meander_fill'),
|
||||
('fill_method', 'circular_fill')])
|
||||
('fill_method', 'circular_fill'),
|
||||
('fill_method', 'linear_gradient_fill')])
|
||||
def expand(self):
|
||||
return self.get_float_param('expand_mm', 0)
|
||||
|
||||
|
@ -254,6 +257,7 @@ class FillStitch(EmbroideryElement):
|
|||
select_items=[('fill_method', 'auto_fill'),
|
||||
('fill_method', 'contour_fill'),
|
||||
('fill_method', 'guided_fill'),
|
||||
('fill_method', 'linear_gradient_fill'),
|
||||
('fill_method', 'legacy_fill')],
|
||||
default=3.0)
|
||||
def max_stitch_length(self):
|
||||
|
@ -270,6 +274,7 @@ class FillStitch(EmbroideryElement):
|
|||
('fill_method', 'contour_fill'),
|
||||
('fill_method', 'guided_fill'),
|
||||
('fill_method', 'circular_fill'),
|
||||
('fill_method', 'linear_gradient_fill'),
|
||||
('fill_method', 'legacy_fill')],
|
||||
default=0.25)
|
||||
def row_spacing(self):
|
||||
|
@ -297,7 +302,10 @@ class FillStitch(EmbroideryElement):
|
|||
'Fractional values are allowed and can have less visible diagonals than integer values.'),
|
||||
type='int',
|
||||
sort_index=25,
|
||||
select_items=[('fill_method', 'auto_fill'), ('fill_method', 'guided_fill'), ('fill_method', 'legacy_fill')],
|
||||
select_items=[('fill_method', 'auto_fill'),
|
||||
('fill_method', 'guided_fill'),
|
||||
('fill_method', 'linear_gradient_fill'),
|
||||
('fill_method', 'legacy_fill')],
|
||||
default=4)
|
||||
def staggers(self):
|
||||
return self.get_float_param("staggers", 4)
|
||||
|
@ -310,7 +318,9 @@ class FillStitch(EmbroideryElement):
|
|||
'Skipping it decreases stitch count and density.'),
|
||||
type='boolean',
|
||||
sort_index=26,
|
||||
select_items=[('fill_method', 'auto_fill'), ('fill_method', 'guided_fill'),
|
||||
select_items=[('fill_method', 'auto_fill'),
|
||||
('fill_method', 'guided_fill'),
|
||||
('fill_method', 'linear_gradient_fill'),
|
||||
('fill_method', 'legacy_fill')],
|
||||
default=False)
|
||||
def skip_last(self):
|
||||
|
@ -329,6 +339,20 @@ class FillStitch(EmbroideryElement):
|
|||
def flip(self):
|
||||
return self.get_boolean_param("flip", False)
|
||||
|
||||
@property
|
||||
@param(
|
||||
'stop_at_ending_point',
|
||||
_('Stop at ending point'),
|
||||
tooltip=_('If this option is disabled, the ending point will only be used to define a general direction for '
|
||||
'stitch routing. When enabled the last section will end at the defined spot.'),
|
||||
type='boolean',
|
||||
sort_index=30,
|
||||
select_items=[('fill_method', 'linear_gradient_fill')],
|
||||
default=False
|
||||
)
|
||||
def stop_at_ending_point(self):
|
||||
return self.get_boolean_param("stop_at_ending_point", False)
|
||||
|
||||
@property
|
||||
@param('underpath',
|
||||
_('Underpath'),
|
||||
|
@ -353,7 +377,8 @@ class FillStitch(EmbroideryElement):
|
|||
select_items=[('fill_method', 'auto_fill'),
|
||||
('fill_method', 'guided_fill'),
|
||||
('fill_method', 'meander_fill'),
|
||||
('fill_method', 'circular_fill')],
|
||||
('fill_method', 'circular_fill'),
|
||||
('fill_method', 'linear_gradient_fill')],
|
||||
sort_index=31)
|
||||
def running_stitch_length(self):
|
||||
return max(self.get_float_param("running_stitch_length_mm", 2.5), 0.01)
|
||||
|
@ -402,6 +427,12 @@ class FillStitch(EmbroideryElement):
|
|||
# SVG spec says the default fill is black
|
||||
return self.get_style("fill", "#000000")
|
||||
|
||||
@property
|
||||
def gradient(self):
|
||||
color = self.color[5:-1]
|
||||
xpath = f'.//svg:defs/svg:linearGradient[@id="{color}"]'
|
||||
return self.node.getroottree().getroot().findone(xpath)
|
||||
|
||||
@property
|
||||
@param('fill_underlay', _('Underlay'), type='toggle', group=_('Fill Underlay'), default=True)
|
||||
def fill_underlay(self):
|
||||
|
@ -427,6 +458,8 @@ class FillStitch(EmbroideryElement):
|
|||
float(angle)) for angle in underlay_angles]
|
||||
except (TypeError, ValueError):
|
||||
return default_value
|
||||
elif self.fill_method == 'linear_gradient_fill' and self.gradient is not None:
|
||||
return [-gradient_angle(self.node, self.gradient)]
|
||||
else:
|
||||
underlay_angles = default_value
|
||||
|
||||
|
@ -704,10 +737,25 @@ class FillStitch(EmbroideryElement):
|
|||
return self.do_legacy_fill()
|
||||
else:
|
||||
stitch_groups = []
|
||||
end = self.get_ending_point()
|
||||
|
||||
for shape in self.shape.geoms:
|
||||
# start and end points
|
||||
start = self.get_starting_point(previous_stitch_group)
|
||||
final_end = self.get_ending_point()
|
||||
|
||||
# sort shapes to get a nicer routing
|
||||
shapes = list(self.shape.geoms)
|
||||
if start:
|
||||
shapes.sort(key=lambda shape: shape.distance(shgeo.Point(start)))
|
||||
else:
|
||||
shapes.sort(key=lambda shape: shape.bounds[0])
|
||||
|
||||
for i, shape in enumerate(shapes):
|
||||
start = self.get_starting_point(previous_stitch_group)
|
||||
if i < len(shapes) - 1:
|
||||
end = nearest_points(shape, shapes[i+1])[0].coords
|
||||
else:
|
||||
end = final_end
|
||||
|
||||
if self.fill_underlay:
|
||||
underlay_shapes = self.underlay_shape(shape)
|
||||
for underlay_shape in underlay_shapes.geoms:
|
||||
|
@ -724,10 +772,18 @@ class FillStitch(EmbroideryElement):
|
|||
stitch_groups.extend(self.do_meander_fill(fill_shape, shape, i, start, end))
|
||||
elif self.fill_method == 'circular_fill':
|
||||
stitch_groups.extend(self.do_circular_fill(fill_shape, previous_stitch_group, start, end))
|
||||
elif self.fill_method == 'linear_gradient_fill':
|
||||
stitch_groups.extend(self.do_linear_gradient_fill(fill_shape, previous_stitch_group, start, end))
|
||||
else:
|
||||
# auto_fill
|
||||
stitch_groups.extend(self.do_auto_fill(fill_shape, previous_stitch_group, start, end))
|
||||
previous_stitch_group = stitch_groups[-1]
|
||||
if stitch_groups:
|
||||
previous_stitch_group = stitch_groups[-1]
|
||||
|
||||
# sort colors of linear gradient (if multiple shapes)
|
||||
if self.fill_method == 'linear_gradient_fill':
|
||||
colors = [stitch_group.color for stitch_group in stitch_groups]
|
||||
stitch_groups.sort(key=lambda group: colors.index(group.color))
|
||||
|
||||
return stitch_groups
|
||||
|
||||
|
@ -746,10 +802,13 @@ class FillStitch(EmbroideryElement):
|
|||
lock_stitches=self.lock_stitches) for stitch_list in stitch_lists]
|
||||
|
||||
def do_underlay(self, shape, starting_point):
|
||||
color = self.color
|
||||
if self.gradient is not None and self.fill_method == 'linear_gradient_fill':
|
||||
color = [style['stop-color'] for style in self.gradient.stop_styles][0]
|
||||
stitch_groups = []
|
||||
for i in range(len(self.fill_underlay_angle)):
|
||||
underlay = StitchGroup(
|
||||
color=self.color,
|
||||
color=color,
|
||||
tags=("auto_fill", "auto_fill_underlay"),
|
||||
lock_stitches=self.lock_stitches,
|
||||
stitches=auto_fill(
|
||||
|
@ -763,7 +822,9 @@ class FillStitch(EmbroideryElement):
|
|||
self.staggers,
|
||||
self.fill_underlay_skip_last,
|
||||
starting_point,
|
||||
underpath=self.underlay_underpath))
|
||||
underpath=self.underlay_underpath
|
||||
)
|
||||
)
|
||||
stitch_groups.append(underlay)
|
||||
starting_point = underlay.stitches[-1]
|
||||
return [stitch_groups, starting_point]
|
||||
|
@ -859,15 +920,9 @@ class FillStitch(EmbroideryElement):
|
|||
starting_point,
|
||||
ending_point,
|
||||
self.underpath,
|
||||
self.guided_fill_strategy,
|
||||
))
|
||||
return [stitch_group]
|
||||
|
||||
def do_meander_fill(self, shape, original_shape, i, starting_point, ending_point):
|
||||
stitch_group = StitchGroup(
|
||||
color=self.color,
|
||||
tags=("meander_fill", "meander_fill_top"),
|
||||
stitches=meander_fill(self, shape, original_shape, i, starting_point, ending_point))
|
||||
self.guided_fill_strategy
|
||||
)
|
||||
)
|
||||
return [stitch_group]
|
||||
|
||||
@cache
|
||||
|
@ -882,6 +937,16 @@ class FillStitch(EmbroideryElement):
|
|||
else:
|
||||
return guide_lines['stroke'][0]
|
||||
|
||||
def do_meander_fill(self, shape, original_shape, i, starting_point, ending_point):
|
||||
stitch_group = StitchGroup(
|
||||
color=self.color,
|
||||
tags=("meander_fill", "meander_fill_top"),
|
||||
stitches=meander_fill(self, shape, original_shape, i, starting_point, ending_point),
|
||||
force_lock_stitches=self.force_lock_stitches,
|
||||
lock_stitches=self.lock_stitches,
|
||||
)
|
||||
return [stitch_group]
|
||||
|
||||
def do_circular_fill(self, shape, last_patch, starting_point, ending_point):
|
||||
# get target position
|
||||
command = self.get_command('ripple_target')
|
||||
|
@ -893,24 +958,29 @@ class FillStitch(EmbroideryElement):
|
|||
else:
|
||||
target = shape.centroid
|
||||
stitches = circular_fill(
|
||||
shape,
|
||||
self.angle,
|
||||
self.row_spacing,
|
||||
self.end_row_spacing,
|
||||
self.staggers,
|
||||
self.running_stitch_length,
|
||||
self.running_stitch_tolerance,
|
||||
self.bean_stitch_repeats,
|
||||
self.repeats,
|
||||
self.skip_last,
|
||||
starting_point,
|
||||
ending_point,
|
||||
self.underpath,
|
||||
target
|
||||
)
|
||||
shape,
|
||||
self.angle,
|
||||
self.row_spacing,
|
||||
self.end_row_spacing,
|
||||
self.staggers,
|
||||
self.running_stitch_length,
|
||||
self.running_stitch_tolerance,
|
||||
self.bean_stitch_repeats,
|
||||
self.repeats,
|
||||
self.skip_last,
|
||||
starting_point,
|
||||
ending_point,
|
||||
self.underpath,
|
||||
target
|
||||
)
|
||||
|
||||
stitch_group = StitchGroup(
|
||||
color=self.color,
|
||||
tags=("circular_fill", "auto_fill_top"),
|
||||
stitches=stitches)
|
||||
stitches=stitches,
|
||||
force_lock_stitches=self.force_lock_stitches,
|
||||
lock_stitches=self.lock_stitches,)
|
||||
return [stitch_group]
|
||||
|
||||
def do_linear_gradient_fill(self, shape, last_patch, start, end):
|
||||
return linear_gradient_fill(self, shape, start, end)
|
||||
|
|
|
@ -7,6 +7,8 @@ from .auto_fill import auto_fill
|
|||
from .circular_fill import circular_fill
|
||||
from .fill import legacy_fill
|
||||
from .guided_fill import guided_fill
|
||||
from .linear_gradient_fill import linear_gradient_fill
|
||||
from .meander_fill import meander_fill
|
||||
|
||||
# Can't put this here because we get a circular import :(
|
||||
# from .auto_satin import auto_satin
|
||||
|
|
|
@ -78,7 +78,7 @@ def auto_fill(shape,
|
|||
segments = [segment for row in rows for segment in row]
|
||||
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
|
||||
|
||||
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
|
||||
if not graph_is_valid(fill_stitch_graph):
|
||||
return fallback(shape, running_stitch_length, running_stitch_tolerance)
|
||||
|
||||
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
|
||||
|
@ -269,7 +269,7 @@ def add_edges_between_outline_nodes(graph, duplicate_every_other=False):
|
|||
check_stop_flag()
|
||||
|
||||
|
||||
def graph_is_valid(graph, shape, max_stitch_length):
|
||||
def graph_is_valid(graph):
|
||||
# The graph may be empty if the shape is so small that it fits between the
|
||||
# rows of stitching. Certain small weird shapes can also cause a non-
|
||||
# eulerian graph.
|
||||
|
|
|
@ -73,7 +73,7 @@ def circular_fill(shape,
|
|||
segments.append([(point.x, point.y) for point in coords])
|
||||
|
||||
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
|
||||
if not graph_is_valid(fill_stitch_graph, shape, running_stitch_length):
|
||||
if not graph_is_valid(fill_stitch_graph):
|
||||
return fallback(shape, running_stitch_length, running_stitch_tolerance)
|
||||
|
||||
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
|
||||
|
@ -124,7 +124,7 @@ def path_to_stitches(shape, path, travel_graph, fill_stitch_graph, running_stitc
|
|||
|
||||
# If the very first stitch is travel, we'll omit it in travel(), so add it here.
|
||||
if not path[0].is_segment():
|
||||
stitches.append(Stitch(*path[0].nodes[0]))
|
||||
stitches.append(Stitch(*path[0].nodes[0], tags={'auto_fill_travel'}))
|
||||
|
||||
for edge in path:
|
||||
if edge.is_segment():
|
||||
|
|
|
@ -39,7 +39,7 @@ def guided_fill(shape,
|
|||
|
||||
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
|
||||
|
||||
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
|
||||
if not graph_is_valid(fill_stitch_graph):
|
||||
return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
|
||||
num_staggers, skip_last, starting_point, ending_point, underpath)
|
||||
|
||||
|
|
|
@ -0,0 +1,341 @@
|
|||
# Authors: see git history
|
||||
#
|
||||
# Copyright (c) 2023 Authors
|
||||
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
||||
|
||||
from math import ceil, floor, sqrt
|
||||
|
||||
import numpy as np
|
||||
from inkex import DirectedLineSegment, Transform
|
||||
from networkx import eulerize
|
||||
from shapely import segmentize
|
||||
from shapely.affinity import rotate
|
||||
from shapely.geometry import LineString, MultiLineString, Point, Polygon
|
||||
|
||||
from ..stitch_plan import StitchGroup
|
||||
from ..svg import get_node_transform
|
||||
from ..utils.threading import check_stop_flag
|
||||
from .auto_fill import (build_fill_stitch_graph, build_travel_graph,
|
||||
find_stitch_path, graph_is_valid)
|
||||
from .circular_fill import path_to_stitches
|
||||
from .guided_fill import apply_stitches
|
||||
|
||||
|
||||
def linear_gradient_fill(fill, shape, starting_point, ending_point):
|
||||
lines, colors, stop_color_line_indices = _get_lines_and_colors(shape, fill)
|
||||
color_lines, colors = _get_color_lines(lines, colors, stop_color_line_indices)
|
||||
if fill.gradient is None:
|
||||
colors.pop()
|
||||
stitch_groups = _get_stitch_groups(fill, shape, colors, color_lines, starting_point, ending_point)
|
||||
return stitch_groups
|
||||
|
||||
|
||||
def _get_lines_and_colors(shape, fill):
|
||||
'''
|
||||
Returns lines and color gradient information
|
||||
lines: a list of lines which cover the whole shape in a 90° angle to the gradient line
|
||||
colors: a list of color values
|
||||
stop_color_line_indices: line indices indicating where color changes are positioned at
|
||||
'''
|
||||
orig_bbox = shape.bounds
|
||||
|
||||
# get angle, colors, as well as start and stop position of the gradient
|
||||
angle, colors, offsets, gradient_start, gradient_end = _get_gradient_info(fill, orig_bbox)
|
||||
|
||||
# get lines
|
||||
lines, bottom_line = _get_lines(fill, shape, orig_bbox, angle)
|
||||
|
||||
gradient_start_line_index = round(bottom_line.project(Point(gradient_start)) / fill.row_spacing)
|
||||
if gradient_start_line_index == 0:
|
||||
gradient_start_line_index = -round(LineString([gradient_start, gradient_end]).project(Point(bottom_line.coords[0])) / fill.row_spacing)
|
||||
stop_color_line_indices = [gradient_start_line_index]
|
||||
gradient_line = LineString([gradient_start, gradient_end])
|
||||
for offset in offsets[1:]:
|
||||
stop_color_line_indices.append(round((gradient_line.length * offset) / fill.row_spacing) + gradient_start_line_index)
|
||||
|
||||
return lines, colors, stop_color_line_indices
|
||||
|
||||
|
||||
def _get_gradient_info(fill, bbox):
|
||||
if fill.gradient is None:
|
||||
# there is no linear gradient, let's simply space out one single color instead
|
||||
angle = fill.angle
|
||||
offsets = [0, 1]
|
||||
colors = [fill.color, 'none']
|
||||
gradient_start = (bbox[0], bbox[1])
|
||||
gradient_end = (bbox[2], bbox[3])
|
||||
else:
|
||||
fill.gradient.apply_transform()
|
||||
offsets = fill.gradient.stop_offsets
|
||||
colors = [style['stop-color'] if float(style['stop-opacity']) > 0 else 'none' for style in fill.gradient.stop_styles]
|
||||
gradient_start, gradient_end = gradient_start_end(fill.node, fill.gradient)
|
||||
angle = gradient_angle(fill.node, fill.gradient)
|
||||
return angle, colors, offsets, gradient_start, gradient_end
|
||||
|
||||
|
||||
def _get_lines(fill, shape, bounding_box, angle):
|
||||
'''
|
||||
To generate the lines we rotate the bounding box to bring the angle in vertical position.
|
||||
From bounds we create a Polygon which we then rotate back, so we receive a rotated bounding box
|
||||
which aligns well to the stitch angle. Combining the points of the subdivided top and bottom line
|
||||
will finally deliver to our stitch rows
|
||||
'''
|
||||
|
||||
# get the rotated bounding box for the shape
|
||||
rotated_shape = rotate(shape, -angle, origin=(bounding_box[0], bounding_box[3]), use_radians=True)
|
||||
bounds = rotated_shape.bounds
|
||||
|
||||
# Generate a Polygon from the rotated bounding box which we then rotate back into original position
|
||||
# extend bounding box for lines just a little to make sure we cover the whole area with lines
|
||||
# this avoids rounding errors due to the rotation later on
|
||||
rot_bbox = Polygon([
|
||||
(bounds[0] - fill.max_stitch_length, bounds[1] - fill.row_spacing),
|
||||
(bounds[2] + fill.max_stitch_length, bounds[1] - fill.row_spacing),
|
||||
(bounds[2] + fill.max_stitch_length, bounds[3] + fill.row_spacing),
|
||||
(bounds[0] - fill.max_stitch_length, bounds[3] + fill.row_spacing)
|
||||
])
|
||||
# and rotate it back into original position
|
||||
rot_bbox = list(rotate(rot_bbox, angle, origin=(bounding_box[0], bounding_box[3]), use_radians=True).exterior.coords)
|
||||
|
||||
# segmentize top and bottom line to finally be ableto generate the stitch lines
|
||||
top_line = LineString([rot_bbox[0], rot_bbox[1]])
|
||||
top = segmentize(top_line, max_segment_length=fill.row_spacing)
|
||||
|
||||
bottom_line = LineString([rot_bbox[3], rot_bbox[2]])
|
||||
bottom = segmentize(bottom_line, max_segment_length=fill.row_spacing)
|
||||
|
||||
lines = list(zip(top.coords, bottom.coords))
|
||||
|
||||
# stagger stitched lines according to user settings
|
||||
staggered_lines = []
|
||||
for i, line in enumerate(lines):
|
||||
staggered_line = apply_stitches(LineString(line), fill.max_stitch_length, fill.staggers, fill.row_spacing, i)
|
||||
staggered_lines.append(staggered_line)
|
||||
return staggered_lines, bottom_line
|
||||
|
||||
|
||||
def _get_color_lines(lines, colors, stop_color_line_indices):
|
||||
'''
|
||||
To define which line will be stitched in which color, we will loop through the color sections
|
||||
defined by the stop positions of the gradient (stop_color_line_indices).
|
||||
Each section will then be subdivided into smaller sections using the square root of the total line number
|
||||
of the whole section. Lines left over from this operation will be added step by step to the smaller sub-sections.
|
||||
Since we do this symmetrically we may end one line short, which we an add at the end.
|
||||
|
||||
Now we define the line colors of the first half of our color section, we will later mirror this on the second half.
|
||||
Therefor we use one additional line of color2 in each sub-section and position them as evenly as possible between the color1 lines.
|
||||
Doing this we take care, that the number of consecutive lines of color1 is always decreasing.
|
||||
|
||||
For example let's take a 12 lines sub-section, with 5 lines of color2.
|
||||
12 / 5 = 2.4
|
||||
12 % 5 = 2
|
||||
This results into the following pattern:
|
||||
xx|xx|x|x|x| (while x = color1 and | = color2).
|
||||
Note that the first two parts have an additional line (as defined by the modulo operation)
|
||||
|
||||
Method returns
|
||||
color_lines: A dictionary with lines grouped by color
|
||||
colors: An updated list of color values.
|
||||
Colors which are positioned outside the shape will be removed.
|
||||
'''
|
||||
|
||||
# create dictionary with a key for each color
|
||||
color_lines = {}
|
||||
for color in colors:
|
||||
color_lines[color] = []
|
||||
|
||||
prev_color = colors[0]
|
||||
prev = None
|
||||
for line_index, color in zip(stop_color_line_indices, colors):
|
||||
if prev is None:
|
||||
if line_index > 0:
|
||||
color_lines[color].extend(lines[0:line_index + 1])
|
||||
prev = line_index
|
||||
prev_color = color
|
||||
continue
|
||||
if prev < 0 and line_index < 0:
|
||||
prev = line_index
|
||||
prev_color = color
|
||||
continue
|
||||
|
||||
prev += 1
|
||||
line_index += 1
|
||||
total_lines = line_index - prev
|
||||
sections = floor(sqrt(total_lines))
|
||||
|
||||
color1 = []
|
||||
color2 = []
|
||||
|
||||
c2_count = 0
|
||||
c1_count = 0
|
||||
current_line = 0
|
||||
|
||||
line_count_diff = floor((total_lines - sections**2) / 2)
|
||||
|
||||
stop = False
|
||||
for i in range(sections):
|
||||
if stop:
|
||||
break
|
||||
|
||||
c2_count += 1
|
||||
c1_count = sections - c2_count
|
||||
rest = c1_count % c2_count
|
||||
c1_count = ceil(c1_count / c2_count)
|
||||
|
||||
current_line, line_count_diff, color1, color2, stop = _add_lines(
|
||||
current_line,
|
||||
total_lines,
|
||||
line_count_diff,
|
||||
color1,
|
||||
color2,
|
||||
stop,
|
||||
rest,
|
||||
c1_count,
|
||||
c2_count
|
||||
)
|
||||
|
||||
# mirror the first half of the color section to receive the full section
|
||||
second_half = color2[-1] * 2 + 1
|
||||
|
||||
color1 = np.array(color1)
|
||||
color2 = np.array(color2)
|
||||
|
||||
c1 = np.append(color1, second_half - color2)
|
||||
color2 = np.append(color2, second_half - color1)
|
||||
color1 = c1
|
||||
|
||||
# until now we only cared about the length of the section
|
||||
# now we need to move it to the correct position
|
||||
color1 += prev
|
||||
color2 += prev
|
||||
|
||||
# add lines to their color key in the dictionary
|
||||
# as sections can start before or after the actual shape we need to make sure,
|
||||
# that we only try to add existing lines
|
||||
color_lines[prev_color].extend([lines[x] for x in color1 if 0 < x < len(lines)])
|
||||
color_lines[color].extend([lines[x] for x in color2 if 0 < x < len(lines)])
|
||||
|
||||
prev = np.max(color2)
|
||||
prev_color = color
|
||||
|
||||
check_stop_flag()
|
||||
|
||||
# add left over lines to last color
|
||||
color_lines[color].extend(lines[prev+1:])
|
||||
|
||||
# remove transparent colors (we just want a gap)
|
||||
color_lines.pop('none', None)
|
||||
|
||||
# remove empty line lists and update colors
|
||||
color_lines = {color: lines for color, lines in color_lines.items() if lines}
|
||||
colors = list(color_lines.keys())
|
||||
|
||||
return color_lines, colors
|
||||
|
||||
|
||||
def _add_lines(current_line, total_lines, line_count_diff, color1, color2, stop, rest, c1_count, c2_count):
|
||||
for j in range(c2_count):
|
||||
if stop:
|
||||
break
|
||||
if rest == 0 or j < rest:
|
||||
count = c1_count
|
||||
else:
|
||||
count = c1_count - 1
|
||||
if line_count_diff > 0:
|
||||
count += 1
|
||||
line_count_diff -= 1
|
||||
for k in range(count):
|
||||
color1.append(current_line)
|
||||
current_line += 1
|
||||
if total_lines / 2 <= current_line + 1:
|
||||
stop = True
|
||||
break
|
||||
color2.append(current_line)
|
||||
current_line += 1
|
||||
return current_line, line_count_diff, color1, color2, stop
|
||||
|
||||
|
||||
def _get_stitch_groups(fill, shape, colors, color_lines, starting_point, ending_point):
|
||||
stitch_groups = []
|
||||
for i, color in enumerate(colors):
|
||||
lines = color_lines[color]
|
||||
|
||||
multiline = MultiLineString(lines).intersection(shape)
|
||||
if not isinstance(multiline, MultiLineString):
|
||||
if isinstance(multiline, LineString):
|
||||
multiline = MultiLineString([multiline])
|
||||
else:
|
||||
continue
|
||||
segments = [list(line.coords) for line in multiline.geoms if len(line.coords) > 1]
|
||||
|
||||
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
|
||||
|
||||
if not graph_is_valid(fill_stitch_graph):
|
||||
# try to eulerize
|
||||
fill_stitch_graph = eulerize(fill_stitch_graph)
|
||||
# still not valid? continue without rendering the color section
|
||||
if not graph_is_valid(fill_stitch_graph):
|
||||
continue
|
||||
|
||||
travel_graph = build_travel_graph(fill_stitch_graph, shape, fill.angle, False)
|
||||
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
|
||||
stitches = path_to_stitches(
|
||||
shape,
|
||||
path,
|
||||
travel_graph,
|
||||
fill_stitch_graph,
|
||||
fill.running_stitch_length,
|
||||
fill.running_stitch_tolerance,
|
||||
fill.skip_last,
|
||||
False # no underpath
|
||||
)
|
||||
|
||||
stitches = _remove_start_end_travel(fill, stitches, colors, i)
|
||||
|
||||
stitch_groups.append(StitchGroup(
|
||||
color=color,
|
||||
tags=("linear_gradient_fill", "auto_fill_top"),
|
||||
stitches=stitches,
|
||||
force_lock_stitches=fill.force_lock_stitches,
|
||||
lock_stitches=fill.lock_stitches,
|
||||
trim_after=fill.has_command("trim") or fill.trim_after
|
||||
))
|
||||
|
||||
return stitch_groups
|
||||
|
||||
|
||||
def _remove_start_end_travel(fill, stitches, colors, color_section):
|
||||
# We can savely remove travel stitches at start since we are changing color all the time
|
||||
# but we do care for the first starting point, it is important when they use an underlay of the same color
|
||||
remove_before = 0
|
||||
if color_section > 0 or not fill.fill_underlay:
|
||||
for stitch in range(len(stitches)-1):
|
||||
if 'auto_fill_travel' not in stitches[stitch].tags:
|
||||
remove_before = stitch
|
||||
break
|
||||
stitches = stitches[remove_before:]
|
||||
remove_after = len(stitches) - 1
|
||||
# We also remove travel stitches at the end. It is optional to the user if the last color block travels
|
||||
# to the defined ending point
|
||||
if color_section < len(colors) - 2 or not fill.stop_at_ending_point:
|
||||
for stitch in range(remove_after, 0, -1):
|
||||
if 'auto_fill_travel' not in stitches[stitch].tags:
|
||||
remove_after = stitch + 1
|
||||
break
|
||||
stitches = stitches[:remove_after]
|
||||
return stitches
|
||||
|
||||
|
||||
def gradient_start_end(node, gradient):
|
||||
transform = Transform(get_node_transform(node))
|
||||
gradient_start = transform.apply_to_point((float(gradient.x1()), float(gradient.y1())))
|
||||
gradient_end = transform.apply_to_point((float(gradient.x2()), float(gradient.y2())))
|
||||
return gradient_start, gradient_end
|
||||
|
||||
|
||||
def gradient_angle(node, gradient):
|
||||
if gradient is None:
|
||||
return
|
||||
gradient_start, gradient_end = gradient_start_end(node, gradient)
|
||||
gradient_line = DirectedLineSegment(gradient_start, gradient_end)
|
||||
return gradient_line.angle
|
|
@ -97,6 +97,7 @@ inkstitch_attribs = [
|
|||
'staggers',
|
||||
'underlay_underpath',
|
||||
'underpath',
|
||||
'stop_at_ending_point',
|
||||
'flip',
|
||||
'clip',
|
||||
# stroke
|
||||
|
|
Ładowanie…
Reference in New Issue