diff options
| -rw-r--r-- | lib/elements/gradient_fill.py | 79 | ||||
| -rw-r--r-- | lib/extensions/__init__.py | 2 | ||||
| -rw-r--r-- | lib/extensions/gradient_blocks.py | 69 | ||||
| -rw-r--r-- | templates/gradient_blocks.xml | 17 |
4 files changed, 167 insertions, 0 deletions
diff --git a/lib/elements/gradient_fill.py b/lib/elements/gradient_fill.py new file mode 100644 index 00000000..5ac49c4e --- /dev/null +++ b/lib/elements/gradient_fill.py @@ -0,0 +1,79 @@ +from math import pi + +from inkex import DirectedLineSegment, Transform +from shapely import geometry as shgeo +from shapely.affinity import affine_transform, rotate +from shapely.ops import split + +from ..svg import get_correction_transform + + +def gradient_shapes_and_attributes(element, shape): + # e.g. url(#linearGradient872) -> linearGradient872 + color = element.color[5:-1] + xpath = f'.//svg:defs/svg:linearGradient[@id="{color}"]' + gradient = element.node.getroottree().getroot().findone(xpath) + gradient.apply_transform() + point1 = (float(gradient.get('x1')), float(gradient.get('y1'))) + point2 = (float(gradient.get('x2')), float(gradient.get('y2'))) + # get 90° angle to calculate the splitting angle + line = DirectedLineSegment(point1, point2) + angle = line.angle - (pi / 2) + # Ink/Stitch somehow turns the stitch angle + stitch_angle = angle * -1 + # create bbox polygon to calculate the length necessary to make sure that + # the gradient splitter lines will cut the entire design + bbox = element.node.bounding_box() + bbox_polygon = shgeo.Polygon([(bbox.left, bbox.top), (bbox.right, bbox.top), + (bbox.right, bbox.bottom), (bbox.left, bbox.bottom)]) + # gradient stops + offsets = gradient.stop_offsets + stop_styles = gradient.stop_styles + # now split the shape according to the gradient stops + polygons = [] + colors = [] + attributes = [] + previous_color = None + end_row_spacing = None + for i, offset in enumerate(offsets): + shape_rest = [] + split_point = shgeo.Point(line.point_at_ratio(float(offset))) + length = split_point.hausdorff_distance(bbox_polygon) + split_line = shgeo.LineString([(split_point.x - length - 2, split_point.y), + (split_point.x + length + 2, split_point.y)]) + split_line = rotate(split_line, angle, origin=split_point, use_radians=True) + transform = -Transform(get_correction_transform(element.node)) + transform = list(transform.to_hexad()) + split_line = affine_transform(split_line, transform) + offset_line = split_line.parallel_offset(1, 'right') + polygon = split(shape, split_line) + color = stop_styles[i]['stop-color'] + # does this gradient line split the shape + offset_outside_shape = len(polygon.geoms) == 1 + for poly in polygon.geoms: + if isinstance(poly, shgeo.Polygon) and element.shape_is_valid(poly): + if poly.intersects(offset_line): + if previous_color: + polygons.append(poly) + colors.append(previous_color) + attributes.append({'angle': stitch_angle, 'end_row_spacing': end_row_spacing, 'color': previous_color}) + polygons.append(poly) + attributes.append({'angle': stitch_angle + pi, 'end_row_spacing': end_row_spacing, 'color': color}) + else: + shape_rest.append(poly) + shape = shgeo.MultiPolygon(shape_rest) + previous_color = color + end_row_spacing = element.row_spacing * 2 + # add left over shape(s) + if shape: + if offset_outside_shape: + for s in shape.geoms: + polygons.append(s) + attributes.append({'color': stop_styles[-2]['stop-color'], 'angle': stitch_angle, 'end_row_spacing': end_row_spacing}) + stitch_angle += pi + else: + end_row_spacing = None + for s in shape.geoms: + polygons.append(s) + attributes.append({'color': stop_styles[-1]['stop-color'], 'angle': stitch_angle, 'end_row_spacing': end_row_spacing}) + return polygons, attributes diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index f1837c59..5c702ce8 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -21,6 +21,7 @@ from .embroider_settings import EmbroiderSettings from .flip import Flip from .generate_palette import GeneratePalette from .global_commands import GlobalCommands +from .gradient_blocks import GradientBlocks from .input import Input from .install import Install from .install_custom_palette import InstallCustomPalette @@ -79,6 +80,7 @@ __all__ = extensions = [StitchPlanPreview, RemoveEmbroiderySettings, Cleanup, BreakApart, + GradientBlocks, ApplyThreadlist, InstallCustomPalette, GeneratePalette, diff --git a/lib/extensions/gradient_blocks.py b/lib/extensions/gradient_blocks.py new file mode 100644 index 00000000..5159149f --- /dev/null +++ b/lib/extensions/gradient_blocks.py @@ -0,0 +1,69 @@ +# Authors: see git history +# +# Copyright (c) 2010 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +from math import degrees + +from inkex import PathElement, errormsg + +from ..elements import FillStitch +from ..elements.gradient_fill import gradient_shapes_and_attributes +from ..i18n import _ +from ..svg import get_correction_transform +from ..svg.tags import INKSTITCH_ATTRIBS +from .base import InkstitchExtension + + +class GradientBlocks(InkstitchExtension): + ''' + This will break apart fill objects with a gradient fill into solid color blocks with end_row_spacing. + ''' + + def effect(self): + if not self.svg.selection: + errormsg(_("Please select at least one object with a gradient fill.")) + return + + if not self.get_elements(): + return + + elements = [element for element in self.elements if (isinstance(element, FillStitch) and self.has_gradient_color(element))] + if not elements: + errormsg(_("Please select at least one object with a gradient fill.")) + return + + for element in elements: + parent = element.node.getparent() + correction_transform = get_correction_transform(element.node) + style = element.node.style + index = parent.index(element.node) + fill_shapes, attributes = gradient_shapes_and_attributes(element, element.shape) + # reverse order so we can always insert with the same index number + fill_shapes.reverse() + attributes.reverse() + for i, shape in enumerate(fill_shapes): + style['fill'] = attributes[i]['color'] + end_row_spacing = attributes[i]['end_row_spacing'] or None + angle = degrees(attributes[i]['angle']) + d = "M " + " ".join([f'{x}, {y}' for x, y in list(shape.exterior.coords)]) + " Z" + block = PathElement(attrib={ + "id": self.uniqueId("path"), + "style": str(style), + "transform": correction_transform, + "d": d, + INKSTITCH_ATTRIBS['angle']: f'{angle: .2f}' + }) + if end_row_spacing: + block.set('inkstitch:end_row_spacing_mm', f'{end_row_spacing: .2f}') + block.set('inkstitch:underpath', False) + parent.insert(index, block) + parent.remove(element.node) + + def has_gradient_color(self, element): + return element.color.startswith('url') and "linearGradient" in element.color + + +if __name__ == '__main__': + e = GradientBlocks() + e.effect() diff --git a/templates/gradient_blocks.xml b/templates/gradient_blocks.xml new file mode 100644 index 00000000..a1f9dea3 --- /dev/null +++ b/templates/gradient_blocks.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension"> + <name>Convert to gradient blocks</name> + <id>org.inkstitch.gradient_blocks</id> + <param name="extension" type="string" gui-hidden="true">gradient_blocks</param> + <effect> + <object-type>all</object-type> + <effects-menu> + <submenu name="Ink/Stitch" translatable="no"> + <submenu name="Tools: Fill" /> + </submenu> + </effects-menu> + </effect> + <script> + {{ command_tag | safe }} + </script> +</inkscape-extension> |
