diff options
| author | Kaalleen <36401965+kaalleen@users.noreply.github.com> | 2023-11-22 20:55:58 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-11-22 20:55:58 +0100 |
| commit | 3bd92265b21d779c41377d2a06d13abaf628fe34 (patch) | |
| tree | 276372bf131626f127045ce826bb8df25d89cb8d /lib | |
| parent | 0b12922d3f058f07020e6afdca26ad34e40918db (diff) | |
Add linear gradient fill (#2587)
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/elements/element.py | 9 | ||||
| -rw-r--r-- | lib/elements/fill_stitch.py | 142 | ||||
| -rw-r--r-- | lib/stitches/__init__.py | 2 | ||||
| -rw-r--r-- | lib/stitches/auto_fill.py | 4 | ||||
| -rw-r--r-- | lib/stitches/circular_fill.py | 4 | ||||
| -rw-r--r-- | lib/stitches/guided_fill.py | 2 | ||||
| -rw-r--r-- | lib/stitches/linear_gradient_fill.py | 341 | ||||
| -rw-r--r-- | lib/svg/tags.py | 1 |
8 files changed, 464 insertions, 41 deletions
diff --git a/lib/elements/element.py b/lib/elements/element.py index 963653af..d6881955 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -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()) diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index b20c2715..bc42163e 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -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): @@ -330,6 +340,20 @@ class FillStitch(EmbroideryElement): 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'), tooltip=_('Travel inside the shape when moving from section to section. 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) @@ -403,6 +428,12 @@ class FillStitch(EmbroideryElement): 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): return self.get_boolean_param("fill_underlay", default=True) @@ -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) diff --git a/lib/stitches/__init__.py b/lib/stitches/__init__.py index f77f16e7..ba56a0ec 100644 --- a/lib/stitches/__init__.py +++ b/lib/stitches/__init__.py @@ -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 diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 83ff226b..24ce6610 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -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. diff --git a/lib/stitches/circular_fill.py b/lib/stitches/circular_fill.py index 351f4cf4..959759dc 100644 --- a/lib/stitches/circular_fill.py +++ b/lib/stitches/circular_fill.py @@ -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(): diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 67ad4ccd..762515f6 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -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) diff --git a/lib/stitches/linear_gradient_fill.py b/lib/stitches/linear_gradient_fill.py new file mode 100644 index 00000000..34f91d5a --- /dev/null +++ b/lib/stitches/linear_gradient_fill.py @@ -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 diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 5713c399..e3d336df 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -97,6 +97,7 @@ inkstitch_attribs = [ 'staggers', 'underlay_underpath', 'underpath', + 'stop_at_ending_point', 'flip', 'clip', # stroke |
