diff options
Diffstat (limited to 'lib/elements')
| -rw-r--r-- | lib/elements/fill_stitch.py | 14 | ||||
| -rw-r--r-- | lib/elements/satin_column.py | 308 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 2 |
3 files changed, 278 insertions, 46 deletions
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 04ddeaea..9b330947 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -891,25 +891,25 @@ class FillStitch(EmbroideryElement): return self.shrink_or_grow_shape(shape, self.expand) def get_starting_point(self, previous_stitch_group): - # If there is a "fill_start" Command, then use that; otherwise pick + # If there is a "starting_point" Command, then use that; otherwise pick # the point closest to the end of the last stitch_group. - if self.get_command('fill_start'): - return self.get_command('fill_start').target_point + if self.get_command('starting_point'): + return self.get_command('starting_point').target_point elif previous_stitch_group: return previous_stitch_group.stitches[-1] else: return None def uses_previous_stitch(self): - if self.get_command('fill_start'): + if self.get_command('starting_point'): return False else: return True def get_ending_point(self): - if self.get_command('fill_end'): - return self.get_command('fill_end').target_point + if self.get_command('ending_point'): + return self.get_command('ending_point').target_point else: return None @@ -1177,7 +1177,7 @@ class FillStitch(EmbroideryElement): def do_circular_fill(self, shape, last_stitch_group, starting_point, ending_point): # get target position - command = self.get_command('ripple_target') + command = self.get_command('target_point') if command: pos = [float(command.use.get("x", 0)), float(command.use.get("y", 0))] transform = get_node_transform(command.use) diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 2353fc1c..49f09e22 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -13,12 +13,12 @@ from inkex import paths from shapely import affinity as shaffinity from shapely import geometry as shgeo from shapely import set_precision -from shapely.ops import nearest_points +from shapely.ops import nearest_points, substring from ..debug.debug import debug from ..i18n import _ from ..metadata import InkStitchMetadata -from ..stitch_plan import StitchGroup +from ..stitch_plan import Stitch, StitchGroup from ..stitches import running_stitch from ..svg import line_strings_to_csp, point_lists_to_csp from ..utils import Point, cache, cut, cut_multiple, offset_points, prng @@ -359,6 +359,46 @@ class SatinColumn(EmbroideryElement): return self.get_boolean_param('swap_satin_rails', False) @property + @param('running_stitch_length_mm', + _('Running stitch length'), + tooltip=_('Length of stitches for start and end point connections.'), + unit='mm', + type='float', + default=2.5, + sort_index=20) + def running_stitch_length(self): + return max(self.get_float_param("running_stitch_length_mm", 2.5), 0.01) + + @property + @param('running_stitch_tolerance_mm', + _('Running stitch tolerance'), + tooltip=_('Determines how hard Ink/Stitch tries to avoid stitching outside the shape.' + 'Lower numbers are less likely to stitch outside the shape but require more stitches.'), + unit='mm', + type='float', + default=0.1, + sort_index=21) + def running_stitch_tolerance(self): + return max(self.get_float_param("running_stitch_tolerance_mm", 0.2), 0.01) + + @property + @param('running_stitch_position', + _('Running Stitch Position'), + tooltip=_('Position of running stitches between the rails. 0% is along the first rail, 50% is centered, 100% is along the second rail.'), + type='float', unit='%', default=50, + sort_index=22) + def running_stitch_position(self): + return min(100, max(0, self.get_float_param("running_stitch_position", 50))) + + @property + @param('start_at_nearest_point', + _('Start at nearest point'), + tooltip=_('Start at nearest point to previous element. A start position command will overwrite this setting.'), + default=False, type='boolean', sort_index=23) + def start_at_nearest_point(self): + return self.get_boolean_param('start_at_nearest_point') + + @property @param('contour_underlay', _('Contour underlay'), type='toggle', group=_('Contour Underlay')) def contour_underlay(self): # "Contour underlay" is stitching just inside the rectangular shape @@ -772,7 +812,7 @@ class SatinColumn(EmbroideryElement): yield NotStitchableError(self.flattened_rails[0].representative_point()) def _center_walk_is_odd(self): - return self.center_walk_underlay_repeats % 2 == 1 + return self.center_walk_underlay and self.center_walk_underlay_repeats % 2 == 1 def reverse(self): """Return a new SatinColumn like this one but in the opposite direction. @@ -807,6 +847,20 @@ class SatinColumn(EmbroideryElement): return self._csp_to_satin(point_lists_to_csp(point_lists)) + def flip(self): + """Return a new SatinColumn like this one but with flipped rails. + + The path will be flattened and the new satin will contain a new XML + node that is not yet in the SVG. + """ + csp = self.path + + if len(csp) > 1: + first, second = self.rail_indices + csp[first], csp[second] = csp[second], csp[first] + + return self._csp_to_satin(csp) + def apply_transform(self): """Return a new SatinColumn like this one but with transforms applied. @@ -837,6 +891,18 @@ class SatinColumn(EmbroideryElement): if cut_points is None: cut_points = self.find_cut_points(split_point) path_lists = self._cut_rails(cut_points) + + # prevent error when split points lies at the start or end of the satin column + cleaned_path_lists = path_lists + for i, path_list in enumerate(path_lists): + if None in path_list: + cleaned_path_lists[i] = None + continue + for path in path_list: + if shgeo.LineString(path).length < self.zigzag_spacing: + cleaned_path_lists[i] = None + path_lists = cleaned_path_lists + self._assign_rungs_to_split_rails(path_lists) self._add_rungs_if_necessary(path_lists) return [self._path_list_to_satins(path_list) for path_list in path_lists] @@ -919,7 +985,8 @@ class SatinColumn(EmbroideryElement): rungs = [shgeo.LineString(self.flatten_subpath(rung)) for rung in self.rungs] for path_list in split_rails: - path_list.extend(rung for rung in rungs if path_list[0].intersects(rung) and path_list[1].intersects(rung)) + if path_list is not None: + path_list.extend(rung for rung in rungs if path_list[0].intersects(rung) and path_list[1].intersects(rung)) def _add_rungs_if_necessary(self, path_lists): """Add an additional rung to each new satin if needed. @@ -936,6 +1003,8 @@ class SatinColumn(EmbroideryElement): """ for path_list in path_lists: + if path_list is None: + continue if len(path_list) in (2, 4): # Add the rung just after the start of the satin. # If the rails have opposite directions it may end up at the end of the satin. @@ -953,7 +1022,10 @@ class SatinColumn(EmbroideryElement): path_list.append(rung) def _path_list_to_satins(self, path_list): - return self._csp_to_satin(line_strings_to_csp(path_list)) + linestrings = line_strings_to_csp(path_list) + if not linestrings: + return None + return self._csp_to_satin(linestrings) def _csp_to_satin(self, csp): node = deepcopy(self.node) @@ -1024,6 +1096,23 @@ class SatinColumn(EmbroideryElement): center_walk = [center_walk[0], center_walk[0]] return shgeo.LineString(center_walk) + @property + @cache + def offset_center_line(self): + stitches = self._get_center_line_stitches() + linestring = shgeo.LineString(stitches) + return linestring + + def _get_center_line_stitches(self): + inset_prop = -np.array([self.running_stitch_position, 100-self.running_stitch_position]) / 100 + + # Do it like contour underlay, but inset all the way to the center. + pairs = self.plot_points_on_rails(self.running_stitch_tolerance, (0, 0), inset_prop) + + points = [points[0] for points in pairs] + stitches = running_stitch.even_running_stitch(points, self.running_stitch_length, self.running_stitch_tolerance) + return stitches + def _stitch_distance(self, pos0, pos1, previous_pos0, previous_pos1): """Return the distance from one stitch to the next.""" @@ -1047,7 +1136,7 @@ class SatinColumn(EmbroideryElement): return max(abs(d0 * normal), abs(d1 * normal)) @debug.time - def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_proportional=(0, 0), use_random=False + def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_proportional=(0, 0), use_random=False, ) -> typing.List[typing.Tuple[Point, Point]]: # Take a section from each rail in turn, and plot out an equal number # of points on both rails. Return the points plotted. The points will @@ -1158,6 +1247,75 @@ class SatinColumn(EmbroideryElement): return pairs + def do_start_path(self, satins, start_point): + start_stitch_group = StitchGroup( + color=self.color, + tags=("satin_column", "satin_column_underlay"), + stitches=[Stitch(*start_point)] + ) + connector = self.offset_center_line + split_line = shgeo.LineString(self.find_cut_points(start_point)) + start = connector.project(nearest_points(split_line, connector)[1]) + + if self.end_point is None: + end = 0 + elif satins[0] is None: + if self._center_walk_is_odd(): + end = 0 + else: + end = connector.length + elif satins[1] is None: + if self._center_walk_is_odd(): + end = connector.length + else: + end = 0 + else: + if not self._center_walk_is_odd(): + end = 0 + else: + split_line = shgeo.LineString(self.find_cut_points(self.end_point)) + end = connector.project(nearest_points(split_line, connector)[1]) + + start_path = substring(connector, start, end) + stitches = [Stitch(*coord) for coord in start_path.coords] + stitch_group = StitchGroup( + color=self.color, + tags=("satin_column", "satin_column_underlay"), + stitches=stitches + ) + stitch_group = self.connect_and_add(start_stitch_group, stitch_group) + return stitch_group + + def do_end_point_connection(self): + if self._center_walk_is_odd(): + return StitchGroup() + center_line = self.offset_center_line.reverse() + stitches = [Stitch(*coord) for coord in center_line.coords] + stitch_group = StitchGroup( + color=self.color, + tags=("satin_column", "satin_column_underlay"), + stitches=stitches + ) + return stitch_group + + def _do_underlay_stitch_groups(self, i, satin, stitch_group): + if self.center_walk_underlay: + center_walk = satin.do_center_walk() + stitch_group = self.connect_and_add(stitch_group, center_walk) + + # if they just went one stitch back, it's not really necessary to add all the underlays + if i == 0 or satin.center_line.length > self.zigzag_spacing: + if self.contour_underlay: + contour = satin.do_contour_underlay() + stitch_group = self.connect_and_add(stitch_group, contour) + + if self.zigzag_underlay: + # zigzag underlay comes after contour walk underlay, so that the + # zigzags sit on the contour walk underlay like rail ties on rails. + zigzag = satin.do_zigzag_underlay() + stitch_group = self.connect_and_add(stitch_group, zigzag) + return stitch_group + def do_contour_underlay(self): # "contour walk" underlay: do stitches up one side and down the # other. if the two sides are far away, adding a running stitch to travel @@ -1196,19 +1354,12 @@ class SatinColumn(EmbroideryElement): def do_center_walk(self): # Center walk underlay is just a running stitch down and back on the # center line between the bezier curves. + repeats = self.center_walk_underlay_repeats - inset_prop = -np.array([self.center_walk_underlay_position, 100-self.center_walk_underlay_position]) / 100 - - # Do it like contour underlay, but inset all the way to the center. - pairs = self.plot_points_on_rails( - self.center_walk_underlay_stitch_tolerance, - (0, 0), inset_prop) - - points = [points[0] for points in pairs] - stitches = running_stitch.even_running_stitch(points, self.center_walk_underlay_stitch_length, self.center_walk_underlay_stitch_tolerance) + stitches = self._get_center_line_stitches() repeated_stitches = [] - for i in range(self.center_walk_underlay_repeats - 1): + for i in range(repeats - 1): if i % 2 == 0: repeated_stitches.extend(reversed(stitches)) else: @@ -1260,6 +1411,17 @@ class SatinColumn(EmbroideryElement): stitch_group.add_tags(("satin_column", "satin_column_underlay", "satin_zigzag_underlay")) return stitch_group + def _do_top_layer_stitch_group(self, satin): + if self.satin_method == 'e_stitch': + stitch_group = satin.do_e_stitch() + elif self.satin_method == 's_stitch': + stitch_group = satin.do_s_stitch() + elif self.satin_method == 'zigzag': + stitch_group = satin.do_zigzag() + else: + stitch_group = satin.do_satin() + return stitch_group + def do_satin(self): # satin: do a zigzag pattern, alternating between the paths. The # zigzag looks like this to make the satin stitches look perpendicular @@ -1410,7 +1572,7 @@ class SatinColumn(EmbroideryElement): if self._center_walk_is_odd(): stitch_group.stitches = list(reversed(stitch_group.stitches)) - stitch_group.add_tags(("satin", "s_stitch")) + stitch_group.add_tags(("satin_column", "s_stitch")) return stitch_group def do_zigzag(self): @@ -1567,51 +1729,121 @@ class SatinColumn(EmbroideryElement): stitch_group.add_stitch(end_stitch) def connect_and_add(self, stitch_group, next_stitch_group): - + if not next_stitch_group.stitches: + return stitch_group if stitch_group.stitches: self.add_running_stitches(stitch_group.stitches[-1], next_stitch_group.stitches[0], stitch_group) stitch_group += next_stitch_group return stitch_group + @property + def start_point(self): + return self._get_command_point('starting_point') + + @property + def end_point(self): + return self._get_command_point('ending_point') + + def _get_command_point(self, command): + point = self.get_command(command) + if point is not None: + point = point.target_point + return point + + def _split_satin(self): + if self.end_point is not None: + satins = self.split(self.end_point) + if self.reverse_rails == 'automatic': + self._adapt_automatic_rail_swapping(satins) + if satins[0] is None: + if not self._center_walk_is_odd(): + satins[1] = satins[1].reverse() + if self.swap_rails: + satins[1] = satins[1].flip() + satins = [None, satins[1]] + elif satins[1] is not None: + if self._center_walk_is_odd(): + satins[0] = satins[0].reverse() + else: + satins[1] = satins[1].reverse() + if self.swap_rails: + satins[0] = satins[0].flip() + else: + if self._center_walk_is_odd(): + satins[0] = satins[0].reverse() + satins = [satins[0], None] + else: + satins = [self, None] + return satins + + def _adapt_automatic_rail_swapping(self, satins): # noqa: C901 + reverse_rails = self._get_rails_to_reverse() + if reverse_rails == (False, False): + if satins[0] is not None: + satins[0].set_param('reverse_rails', 'none') + if satins[1] is not None: + satins[1].set_param('reverse_rails', 'none') + elif reverse_rails == (True, False): + if satins[0] is not None: + satins[0].set_param('reverse_rails', 'first') + if satins[1] is not None: + satins[1].set_param('reverse_rails', 'first') + elif reverse_rails == (False, True): + if satins[0] is not None: + satins[0].set_param('reverse_rails', 'second') + if satins[1] is not None: + satins[1].set_param('reverse_rails', 'second') + elif reverse_rails == (True, True): + if satins[0] is not None: + satins[0].set_param('reverse_rails', 'both') + if satins[1] is not None: + satins[1].set_param('reverse_rails', 'both') + def to_stitch_groups(self, last_stitch_group=None): # Stitch a variable-width satin column, zig-zagging between two paths. # The algorithm will draw zigzags between each consecutive pair of # beziers. The boundary points between beziers serve as "checkpoints", # allowing the user to control how the zigzags flow around corners. + satins = self._split_satin() + stitch_group = StitchGroup( color=self.color, force_lock_stitches=self.force_lock_stitches, lock_stitches=self.lock_stitches ) - if self.center_walk_underlay: - stitch_group += self.do_center_walk() + start_point = self.start_point + if start_point is None and last_stitch_group is not None and self.start_at_nearest_point: + start_point = nearest_points(shgeo.Point(*last_stitch_group.stitches[-1]), self.center_line)[1] + start_point = Point(*list(start_point.coords[0])) + if start_point is not None: + start_path = self.do_start_path(satins, start_point) + stitch_group = self.connect_and_add(stitch_group, start_path) - if self.contour_underlay: - contour = self.do_contour_underlay() - stitch_group = self.connect_and_add(stitch_group, contour) - - if self.zigzag_underlay: - # zigzag underlay comes after contour walk underlay, so that the - # zigzags sit on the contour walk underlay like rail ties on rails. - zigzag = self.do_zigzag_underlay() - stitch_group = self.connect_and_add(stitch_group, zigzag) + for i, satin in enumerate(satins): + if satin is None: + continue - if self.satin_method == 'e_stitch': - final_stitch_group = self.do_e_stitch() - elif self.satin_method == 's_stitch': - final_stitch_group = self.do_s_stitch() - elif self.satin_method == 'zigzag': - final_stitch_group = self.do_zigzag() - else: - final_stitch_group = self.do_satin() + if i > 0 and None not in satins: + end_point_connection = satin.do_end_point_connection() + stitch_group = self.connect_and_add(stitch_group, end_point_connection) - stitch_group = self.connect_and_add(stitch_group, final_stitch_group) + stitch_group = self._do_underlay_stitch_groups(i, satin, stitch_group) + final_stitch_group = self._do_top_layer_stitch_group(satin) + stitch_group = self.connect_and_add(stitch_group, final_stitch_group) if not stitch_group.stitches: return [] + if self.end_point: + ending_point_stitch_group = StitchGroup( + color=self.color, + tags=("satin_column"), + stitches=[Point(*self.end_point)] + ) + stitch_group = self.connect_and_add(stitch_group, ending_point_stitch_group) + return [stitch_group] diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index c0da08fe..ff9718c5 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -515,7 +515,7 @@ class Stroke(EmbroideryElement): return coords def get_ripple_target(self): - command = self.get_command('ripple_target') + command = self.get_command('target_point') if command: pos = [float(command.use.get("x", 0)), float(command.use.get("y", 0))] transform = get_node_transform(command.use) |
