diff options
| author | George Steel <george.steel@gmail.com> | 2022-11-23 21:59:37 -0500 |
|---|---|---|
| committer | George Steel <george.steel@gmail.com> | 2022-11-23 21:59:37 -0500 |
| commit | 74f0943cc8e73a68b84d69acc1482bf1657e3ad6 (patch) | |
| tree | bca22600fc3d0730a75171945ee9fd4d9f755ce9 /lib/elements | |
| parent | 7cef2c9a094abe61282ac9bdc6bbfdc1e3e1d8c6 (diff) | |
| parent | df4c5cd59b0e391048c8fd2271893dbd49ec7cd3 (diff) | |
Merge branch 'main' of https://github.com/inkstitch/inkstitch into george-steel/pull-comp-percent
Diffstat (limited to 'lib/elements')
| -rw-r--r-- | lib/elements/clone.py | 81 | ||||
| -rw-r--r-- | lib/elements/fill_stitch.py | 3 | ||||
| -rw-r--r-- | lib/elements/gradient_fill.py | 79 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 23 |
4 files changed, 156 insertions, 30 deletions
diff --git a/lib/elements/clone.py b/lib/elements/clone.py index d9185012..b5507e4f 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -3,12 +3,13 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -from math import atan, degrees +from math import atan2, degrees, radians + +from inkex import CubicSuperPath, Path, Transform from ..commands import is_command_symbol from ..i18n import _ from ..svg.path import get_node_transform -from ..svg.svg import find_elements from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_USE_TAG, XLINK_HREF) from ..utils import cache @@ -60,7 +61,16 @@ class Clone(EmbroideryElement): type='float') @cache def clone_fill_angle(self): - return self.get_float_param('angle', 0) + return self.get_float_param('angle') or None + + @property + @param('flip_angle', + _('Flip angle'), + tooltip=_("Flip automatically calucalted angle if it appears to be wrong."), + type='boolean') + @cache + def flip_angle(self): + return self.get_boolean_param('flip_angle') def clone_to_element(self, node): from .utils import node_to_elements @@ -73,33 +83,51 @@ class Clone(EmbroideryElement): if source_node.tag not in EMBROIDERABLE_TAGS: return [] - self.node.style = source_node.specified_style() - - # a. a custom set fill angle - # b. calculated rotation for the cloned fill element to look exactly as it's source - param = INKSTITCH_ATTRIBS['angle'] - if self.clone_fill_angle is not None: - angle = self.clone_fill_angle + old_transform = source_node.get('transform', '') + source_transform = source_node.composed_transform() + source_path = Path(source_node.get_path()).transform(source_transform) + transform = Transform(source_node.get('transform', '')) @ -source_transform + transform @= self.node.composed_transform() @ Transform(source_node.get('transform', '')) + source_node.set('transform', transform) + + old_angle = float(source_node.get(INKSTITCH_ATTRIBS['angle'], 0)) + if self.clone_fill_angle is None: + rot = transform.add_rotate(-old_angle) + angle = self._get_rotation(rot, source_node, source_path) + if angle is not None: + source_node.set(INKSTITCH_ATTRIBS['angle'], angle) else: - # clone angle - clone_mat = self.node.composed_transform() - clone_angle = degrees(atan(-clone_mat[1][0]/clone_mat[1][1])) - # source node angle - source_mat = source_node.composed_transform() - source_angle = degrees(atan(-source_mat[1][0]/source_mat[1][1])) - # source node fill angle - source_fill_angle = source_node.get(param, 0) - - angle = clone_angle + float(source_fill_angle) - source_angle - self.node.set(param, str(angle)) - - elements = self.clone_to_element(self.node) + source_node.set(INKSTITCH_ATTRIBS['angle'], self.clone_fill_angle) + elements = self.clone_to_element(source_node) for element in elements: - patches.extend(element.to_stitch_groups(last_patch)) + stitch_groups = element.to_stitch_groups(last_patch) + patches.extend(stitch_groups) + source_node.set('transform', old_transform) + source_node.set(INKSTITCH_ATTRIBS['angle'], old_angle) return patches + def _get_rotation(self, transform, source_node, source_path): + try: + rotation = transform.rotation_degrees() + except ValueError: + source_path = CubicSuperPath(source_path)[0] + clone_path = Path(source_node.get_path()).transform(source_node.composed_transform()) + clone_path = CubicSuperPath(clone_path)[0] + + angle_source = atan2(source_path[1][1][1] - source_path[0][1][1], source_path[1][1][0] - source_path[0][1][0]) + angle_clone = atan2(clone_path[1][1][1] - clone_path[0][1][1], clone_path[1][1][0] - clone_path[0][1][0]) + angle_embroidery = radians(-float(source_node.get(INKSTITCH_ATTRIBS['angle'], 0))) + + diff = angle_source - angle_embroidery + rotation = degrees(diff + angle_clone) + + if self.flip_angle: + rotation = -degrees(diff - angle_clone) + + return -rotation + def get_clone_style(self, style_name, node, default=None): style = node.style[style_name] or default return style @@ -132,7 +160,4 @@ def is_embroiderable_clone(node): def get_clone_source(node): - source_id = node.get(XLINK_HREF)[1:] - xpath = ".//*[@id='%s']" % (source_id) - source_node = find_elements(node, xpath)[0] - return source_node + return node.href diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index ca44e3cc..a5248952 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -216,7 +216,8 @@ class FillStitch(EmbroideryElement): @property @param('staggers', _('Stagger rows this many times before repeating'), - tooltip=_('Length of the cycle by which successive stitch rows are staggered. Fractional values are allowed and can have less visible diagonals than integer values.'), + tooltip=_('Length of the cycle by which successive stitch rows are staggered.' + 'Fractional values are allowed and can have less visible diagonals than integer values.'), type='int', sort_index=6, select_items=[('fill_method', 0), ('fill_method', 2), ('fill_method', 3)], 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/elements/stroke.py b/lib/elements/stroke.py index ce973c0e..2854adaf 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -39,6 +39,14 @@ class MultipleGuideLineWarning(ValidationWarning): ] +class SmallZigZagWarning(ValidationWarning): + name = _("Small ZigZag") + description = _("This zig zag stitch has a stroke width smaller than 0.5 units.") + steps_to_solve = [ + _("Set your stroke to be dashed to indicate running stitch. Any kind of dash will work.") + ] + + class Stroke(EmbroideryElement): element_name = _("Stroke") @@ -481,6 +489,15 @@ class Stroke(EmbroideryElement): return guide_lines['satin'][0] return guide_lines['stroke'][0] + def _representative_point(self): + # if we just take the center of a line string we could end up on some point far away from the actual line + try: + coords = list(self.shape.coords) + except NotImplementedError: + # linear rings to not have a coordinate sequence + coords = list(self.shape.exterior.coords) + return coords[int(len(coords)/2)] + def validation_warnings(self): if self.stroke_method == 1 and self.skip_start + self.skip_end >= self.line_count: yield IgnoreSkipValues(self.shape.centroid) @@ -489,4 +506,8 @@ class Stroke(EmbroideryElement): if self.stroke_method == 1: guide_lines = get_marker_elements(self.node, "guide-line", False, True, True) if sum(len(x) for x in guide_lines.values()) > 1: - yield MultipleGuideLineWarning(self.shape.centroid) + yield MultipleGuideLineWarning(self._representative_point()) + + stroke_width, units = parse_length_with_units(self.get_style("stroke-width", "1")) + if not self.dashed and stroke_width <= 0.5: + yield SmallZigZagWarning(self._representative_point()) |
