diff options
| author | Lex Neva <lexelby@users.noreply.github.com> | 2023-07-30 10:49:25 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-07-30 10:49:25 -0400 |
| commit | 9c090170941e00a6c6265e3a6999ca240fb73052 (patch) | |
| tree | 748e9bdda3acbf145c8ddb2ba7595be1975f6e56 /lib | |
| parent | 96df68c46f495de0f63d36b5130e7dfdc773198a (diff) | |
| parent | 74e93834c094351a398dd46d01c40d197f8fe9af (diff) | |
Merge pull request #2418 from inkstitch/lexelby/convert-to-satin-join
produce only one satin from convert to satin
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/elements/satin_column.py | 40 | ||||
| -rw-r--r-- | lib/extensions/convert_to_satin.py | 71 | ||||
| -rw-r--r-- | lib/svg/path.py | 15 |
3 files changed, 93 insertions, 33 deletions
diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 86c6d05a..4ea26575 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -713,6 +713,9 @@ class SatinColumn(EmbroideryElement): Returns two new SatinColumn instances: the part before and the part after the split point. All parameters are copied over to the new SatinColumn instances. + + The returned SatinColumns will not be in the SVG document and will have + their transforms applied. """ cut_points = self._find_cut_points(split_point) @@ -825,6 +828,43 @@ class SatinColumn(EmbroideryElement): return SatinColumn(node) + def merge(self, satin): + """Merge this satin with another satin + + This method expects that the provided satin continues on directly after + this one, as would be the case, for example, if the two satins were the + result of the split() method. + + Returns a new SatinColumn instance that combines the rails and rungs of + this satin and the provided satin. A rung is added at the end of this + satin. + + The returned SatinColumn will not be in the SVG document and will have + its transforms applied. + """ + rails = [self.flatten_subpath(rail) for rail in self.rails] + other_rails = [satin.flatten_subpath(rail) for rail in satin.rails] + + if len(rails) != 2 or len(other_rails) != 2: + # weird non-satin things, give up and don't merge + return self + + # remove first node of each other rail before merging (avoid duplicated nodes) + rails[0].extend(other_rails[0][1:]) + rails[1].extend(other_rails[1][1:]) + + rungs = [self.flatten_subpath(rung) for rung in self.rungs] + other_rungs = [satin.flatten_subpath(rung) for rung in satin.rungs] + + # add a rung in between the two satins and extend it just a litte to ensure it is crossing the rails + new_rung = shgeo.LineString([other_rails[0][0], other_rails[1][0]]) + rungs.append(list(shaffinity.scale(new_rung, 1.2, 1.2).coords)) + + # add on the other satin's rungs + rungs.extend(other_rungs) + + return self._csp_to_satin(point_lists_to_csp(rails + rungs)) + @property @cache def center_line(self): diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py index 7a36ce21..4bb3588e 100644 --- a/lib/extensions/convert_to_satin.py +++ b/lib/extensions/convert_to_satin.py @@ -13,7 +13,7 @@ from numpy import diff, setdiff1d, sign from shapely import geometry as shgeo from .base import InkstitchExtension -from ..elements import Stroke +from ..elements import SatinColumn, Stroke from ..i18n import _ from ..svg import PIXELS_PER_MM, get_correction_transform from ..svg.tags import INKSTITCH_ATTRIBS @@ -51,22 +51,28 @@ class ConvertToSatin(InkstitchExtension): path_style = self.path_style(element) for path in element.paths: - path = self.remove_duplicate_points(path) + path = self.remove_duplicate_points(self.fix_loop(path)) if len(path) < 2: # ignore paths with just one point -- they're not visible to the user anyway continue - for satin in self.convert_path_to_satins(path, element.stroke_width, style_args, correction_transform, path_style): - parent.insert(index, satin) - index += 1 + satins = list(self.convert_path_to_satins(path, element.stroke_width, style_args, path_style)) + + if satins: + joined_satin = satins[0] + for satin in satins[1:]: + joined_satin = joined_satin.merge(satin) + + joined_satin.node.set('transform', correction_transform) + parent.insert(index, joined_satin.node) parent.remove(element.node) - def convert_path_to_satins(self, path, stroke_width, style_args, correction_transform, path_style, depth=0): + def convert_path_to_satins(self, path, stroke_width, style_args, path_style, depth=0): try: rails, rungs = self.path_to_satin(path, stroke_width, style_args) - yield self.satin_to_svg_node(rails, rungs, correction_transform, path_style) + yield SatinColumn(self.satin_to_svg_node(rails, rungs, path_style)) except SelfIntersectionError: # The path intersects itself. Split it in two and try doing the halves # individually. @@ -76,27 +82,37 @@ class ConvertToSatin(InkstitchExtension): # getting nowhere. Just give up on this section of the path. return - half = int(len(path) / 2.0) - halves = [path[:half + 1], path[half:]] + halves = self.split_path(path) for path in halves: - for satin in self.convert_path_to_satins(path, stroke_width, style_args, correction_transform, path_style, depth=depth + 1): + for satin in self.convert_path_to_satins(path, stroke_width, style_args, path_style, depth=depth + 1): yield satin + def split_path(self, path): + half = len(path) // 2 + halves = [path[:half], path[half:]] + + start = Point.from_tuple(halves[0][-1]) + end = Point.from_tuple(halves[1][0]) + + midpoint = (start + end) / 2 + midpoint = midpoint.as_tuple() + + halves[0].append(midpoint) + halves[1] = [midpoint] + halves[1] + + return halves + def fix_loop(self, path): - if path[0] == path[-1]: - # Looping paths seem to confuse shapely's parallel_offset(). It loses track - # of where the start and endpoint is, even if the user explicitly breaks the - # path. I suspect this is because parallel_offset() uses buffer() under the - # hood. - # - # To work around this we'll introduce a tiny gap by nudging the starting point - # toward the next point slightly. - start = Point(*path[0]) - next = Point(*path[1]) - direction = (next - start).unit() - start += 0.01 * direction - path[0] = start.as_tuple() + if path[0] == path[-1] and len(path) > 1: + first = Point.from_tuple(path[0]) + second = Point.from_tuple(path[1]) + midpoint = (first + second) / 2 + midpoint = midpoint.as_tuple() + + return [midpoint] + path[1:] + [path[0], midpoint] + else: + return path def remove_duplicate_points(self, path): path = [[round(coord, 4) for coord in point] for point in path] @@ -304,10 +320,8 @@ class ConvertToSatin(InkstitchExtension): # Rotate 90 degrees left to make a normal vector. normal = tangent.rotate_left() - # Travel 75% of the stroke width left and right to make the rung's - # endpoints. This means the rung's length is 150% of the stroke - # width. - offset = normal * stroke_width * 0.75 + # Extend the rungs by an offset value to make sure they will cross the rails + offset = normal * (stroke_width / 2) * 1.2 rung_start = rung_center + offset rung_end = rung_center - offset @@ -319,7 +333,7 @@ class ConvertToSatin(InkstitchExtension): color = element.get_style('stroke', '#000000') return "stroke:%s;stroke-width:1px;fill:none" % (color) - def satin_to_svg_node(self, rails, rungs, correction_transform, path_style): + def satin_to_svg_node(self, rails, rungs, path_style): d = "" for path in chain(rails, rungs): d += "M" @@ -330,7 +344,6 @@ class ConvertToSatin(InkstitchExtension): return inkex.PathElement(attrib={ "id": self.uniqueId("path"), "style": path_style, - "transform": correction_transform, "d": d, INKSTITCH_ATTRIBS['satin_column']: "true", }) diff --git a/lib/svg/path.py b/lib/svg/path.py index 6c2cbe35..878d2a7c 100644 --- a/lib/svg/path.py +++ b/lib/svg/path.py @@ -53,11 +53,18 @@ def get_node_transform(node): def get_correction_transform(node, child=False): - """Get a transform to apply to new siblings or children of this SVG node""" + """Get a transform to apply to new siblings or children of this SVG node - # if we want to place our new nodes in the same group/layer as this node, - # then we'll need to factor in the effects of any transforms set on - # the parents of this node. + Arguments: + child (boolean) -- whether the new nodes we're going to add will be + children of node (child=True) or siblings of node + (child=False) + + This allows us to add a new child node that has its path specified in + absolute coordinates. The correction transform will undo the effects of + the parent's and ancestors' transforms so that absolute coordinates + work properly. + """ if child: transform = get_node_transform(node) |
